diff --git a/.flake8 b/.flake8 deleted file mode 100644 index bc16abad..00000000 --- a/.flake8 +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -extend-ignore = E203 -exclude = .git,__pycache__,docs/source/conf.py,old,build,dist -max-complexity = 100 -max-line-length = 160 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae0a4d3d..d1bcdf42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,7 +60,7 @@ jobs: run: uv sync --extra dev - name: Run pytest (fast — excludes @pytest.mark.slow) - run: uv run pytest tests/ -m "not slow" -n auto --tb=short + run: uv run pytest tests/ -m "not slow" -n auto --tb=short --cov=src/spotoptim --cov-branch --cov-report=term # ── Full suite: nightly schedule + manual dispatch ───────────────────────── test-full: @@ -118,3 +118,7 @@ jobs: - name: Ruff run: uv run ruff check src/ tests/ continue-on-error: true + + - name: Type check with ty (non-blocking) + run: uv run ty check src/ + continue-on-error: true diff --git a/PARALLELIZATION_STRATEGY.md b/PARALLELIZATION_STRATEGY.md deleted file mode 100644 index 59e7624d..00000000 --- a/PARALLELIZATION_STRATEGY.md +++ /dev/null @@ -1,391 +0,0 @@ -# Parallelization Strategy for spotoptim - -**Document version:** 2026-03-15 -**Applies to:** spotoptim ≥ 0.6.0 -**Implementation status:** Improvements E (0.7.0), C (0.8.0), F (0.9.0), and D (0.10.0) are complete. - ---- - -## 1. Current Implementation (0.6.0) - -### 1.1 Overview - -spotoptim implements **steady-state asynchronous Bayesian Optimization (BO)** -using `concurrent.futures.ProcessPoolExecutor`. -The entry point is the `n_jobs` constructor parameter (default `1`). - -``` -n_jobs == 1 → optimize_sequential_run() (single process, synchronous) -n_jobs > 1 → optimize_steady_state() (multi-process, asynchronous) -``` - -### 1.2 Steady-State Architecture - -When `n_jobs > 1` the optimizer maintains a live pool of at most `n_jobs` -concurrent futures, split into two task types: - -| Task type | What it does | Submitted by | -|-----------|-------------|--------------| -| `search` | Serializes the entire SpotOptim instance with `dill`, deserializes it in a worker process, calls `suggest_next_infill_point()` | Main process after every surrogate refit | -| `eval` | Serializes `(optimizer, x)` with `dill`, evaluates the objective function `fun(x)` in a worker process | Main process immediately after a `search` result arrives | - -The main loop uses `concurrent.futures.wait(..., return_when=FIRST_COMPLETED)`, -so new tasks are dispatched as soon as any slot frees — there is no synchronous -barrier between generations. - -``` -Main process -│ -├─ ProcessPoolExecutor (n_jobs workers) -│ ├─ Worker 0: search task → returns x_cand_0 -│ ├─ Worker 1: eval task → returns (x, y) -│ └─ Worker 2: search task → returns x_cand_2 -│ -│ On FIRST_COMPLETED: -│ ├─ If eval done → update X_, y_, refit surrogate, dispatch new search -│ └─ If search done → dispatch eval(x_cand) -``` - -### 1.3 Why `dill` Instead of `pickle` - -The standard `multiprocessing` and `concurrent.futures` serialization uses -`pickle`, which cannot handle lambda functions, local functions, or closures. -spotoptim accepts any callable `fun`, so `dill` is used to serialize the -full optimizer object (including the user-supplied `fun`) to each worker. - -### 1.4 Where `differential_evolution` Parallelism Is **Not** Used - -SciPy's `differential_evolution` (the default acquisition optimizer) supports a -`workers=` parameter that distributes population evaluation across -`multiprocessing.Pool` workers. spotoptim does **not** use this parameter: - -```python -result = differential_evolution( - func=self._acquisition_function, - bounds=self.bounds, - vectorized=True, # ← 18× batch speedup via NumPy; used instead of workers= - ... -) -``` - -`workers > 1` in SciPy overrides `vectorized=True`, discarding the batch -speedup that is already in place. Because the acquisition function is cheap -surrogate prediction (NumPy/BLAS), process-spawn overhead would outweigh any -parallelism benefit. - ---- - -## 2. Comparison with SciPy and Python `multiprocessing` - -### 2.1 Level of Parallelism - -The two systems parallelize at **different levels**: - -``` -BO outer loop [spotoptim n_jobs= parallelizes here] -│ -└─ BO iteration - ├─ Fit surrogate - └─ Optimize acquisition function - └─ DE inner loop [SciPy workers= would parallelize here] - └─ Evaluate pop member (surrogate.predict) -``` - -spotoptim's `n_jobs` targets the **expensive** part (objective function -evaluations) across BO iterations; SciPy's `workers` targets **cheap** -surrogate predictions within a single DE run. - -### 2.2 Mechanism Comparison - -| Dimension | SciPy DE `workers=` | spotoptim `n_jobs=` | -|-----------|--------------------|--------------------| -| Executor | `multiprocessing.Pool.map()` | `concurrent.futures.ProcessPoolExecutor` | -| Serialization | `pickle` | `dill` | -| Blocking model | Synchronous (`map` waits for all) | Asynchronous (`FIRST_COMPLETED`) | -| Parallelizes | Population evaluation (inner) | Objective evaluation (outer) | -| Surrogate updates | After each DE generation | After each objective evaluation | -| Callable constraint | Must be `pickle`-able | Any callable (lambdas, closures) | - -### 2.3 Strengths of the Current spotoptim Approach - -1. **Right level for expensive objectives.** BO is used when `fun` is slow - (seconds to hours per call). Parallelizing evaluations is where the - wall-clock gain is. -2. **Asynchronous steady-state.** No worker idles waiting for the slowest - peer in a synchronous batch. -3. **Continuous surrogate updates.** Every completed evaluation immediately - refits the model, so subsequent searches use fresher information than a - fully-synchronous scheme would provide. -4. **`dill` serialization.** Users can pass lambdas and local functions as - `fun` without restrictions. - ---- - -## 3. Identified Weaknesses and the Improvement Roadmap - -The following weaknesses were identified. Two proposed improvements -(shared memory and SciPy `workers=`) were **discarded** because they conflict -with existing features or are superseded by better alternatives — see -[Section 4](#4-discarded-improvements). - -Four compatible improvements are retained and ordered by -dependency and risk: - -| # | Improvement | Release | Depends on | Status | -|---|-------------|---------|------------|--------| -| E | `n_jobs=-1` convention | 0.7.0 | — | ✅ Done | -| C | ThreadPoolExecutor for `search` tasks | 0.8.0 | E | ✅ Done | -| F | Batch evaluation API | 0.9.0 | C | ✅ Done | -| D | Free-threaded (no-GIL) awareness | 0.10.0 | C | ✅ Done | - ---- - -## 4. Discarded Improvements - -### 4.1 A — Shared Memory for `X_`, `y_` (Superseded by C) - -**Idea:** Use `multiprocessing.shared_memory.SharedMemory` to expose the design -matrix `X_` and target vector `y_` without copying them to each worker process. - -**Why discarded:** Improvement C (ThreadPoolExecutor for search tasks) makes -this unnecessary. Threads share the process heap automatically; there is no -copy of `X_` or `y_` across process boundaries for search tasks. Eval tasks -receive only a single point `x`, never the full dataset. Implementing -shared memory alongside threads would add complexity with no remaining benefit. - -### 4.2 B — `workers=` for `differential_evolution` (Conflicts with `vectorized=True`) - -**Idea:** Pass `workers=n_jobs` to `scipy.optimize.differential_evolution` -to parallelize population evaluation within the acquisition optimizer. - -**Why discarded:** SciPy explicitly overrides `vectorized=True` when -`workers != 1` (see SciPy docs). The current 18× batch speedup from -`vectorized=True` would be lost. Because the acquisition function is cheap -surrogate prediction (NumPy/BLAS operations), process-spawn and IPC overhead -would dominate any parallelism benefit. The correct place to add parallelism -is the outer objective evaluation, not the inner surrogate query. - ---- - -## 5. Release Roadmap - -### Release 0.7.0 — Improvement E: `n_jobs=-1` Convention ✅ - -**Current problem:** spotoptim does not follow the scikit-learn / SciPy -convention that `n_jobs=-1` means "use all available CPU cores". Users who -set `n_jobs=-1` currently get `ProcessPoolExecutor(max_workers=-1)`, which -raises a `ValueError`. - -**Change:** Resolve `n_jobs` to `os.cpu_count()` (or a safe fallback of `1`) -when `-1` is passed, before the executor is constructed: - -```python -import os - -def _resolve_n_jobs(n_jobs: int) -> int: - if n_jobs == -1: - return os.cpu_count() or 1 - if n_jobs < -1 or n_jobs == 0: - raise ValueError(f"n_jobs must be a positive integer or -1, got {n_jobs}.") - return n_jobs -``` - -**Compatibility:** Purely additive. No existing behaviour changes when -`n_jobs >= 1`. - -**Risk:** Minimal. - ---- - -### Release 0.8.0 — Improvement C: ThreadPoolExecutor for `search` Tasks ✅ - -**Current problem:** Every `search` task (optimizing the acquisition function) -requires: - -1. `dill.dumps(self)` — serializing the entire SpotOptim instance (surrogate - model, `X_`, `y_`, config, …) into a byte string -2. IPC to a worker process -3. `dill.loads(pickled_optimizer)` — full deserialization in the worker -4. `suggest_next_infill_point()` — the actual work -5. Serialization of the result back to the main process - -Steps 1–3 and 5 are pure overhead. For complex surrogates (e.g. MLPSurrogate -with PyTorch weights) the serialized object can be several megabytes, adding -tens to hundreds of milliseconds **per search task**. - -**Key insight:** The search task is **not** CPU-bound in the way that justifies -process isolation. It calls surrogate model prediction (NumPy matrix -operations, PyTorch inference), which releases the GIL and benefits from -shared L2/L3 cache. Threads are the correct primitive here. - -**Change:** Use a *hybrid* executor design: - -```python -from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, wait, FIRST_COMPLETED - -with ProcessPoolExecutor(max_workers=n_jobs) as eval_pool, \ - ThreadPoolExecutor(max_workers=n_jobs) as search_pool: - - # eval tasks → eval_pool (process isolation; objective fun may be arbitrary) - # search tasks → search_pool (thread; surrogate shared from main process heap) - ... -``` - -The `search` task no longer needs `dill` serialization at all — the worker -thread sees the same Python objects as the main thread: - -```python -# Before (process-based, requires full serialization) -pickled_opt = dill.dumps(self) -fut = eval_pool.submit(remote_search_task, pickled_opt) - -# After (thread-based, zero copy) -fut = search_pool.submit(self.suggest_next_infill_point) -``` - -**`eval` tasks** remain in the `ProcessPoolExecutor` because: - -- The user's objective `fun` may be arbitrary (could hold the GIL, call - subprocesses, or have side-effects that require isolation) -- Process isolation prevents a crashing eval from killing the main process - -**Expected benefit:** Eliminates the dominant serialization bottleneck for -search tasks. The surrogate model and all data are shared by reference. -Memory footprint per active search task drops from O(surrogate size) to O(1). - -**Compatibility:** The public API (`n_jobs`, `optimize()`) does not change. -The internal `optimize_steady_state` method is refactored. - -**Risk:** Low. Thread safety must be verified for surrogate `predict()` calls -made concurrently. sklearn's `GaussianProcessRegressor.predict` is -thread-safe for read-only inference. PyTorch `model.forward()` with -`torch.no_grad()` is thread-safe. The main-thread surrogate `fit()` must not -run concurrently with a `search` thread's `predict()`; this is already -guaranteed by the steady-state design (refit happens only after an `eval` -completes, at which point no search task is in flight for the stale model). - ---- - -### Release 0.9.0 — Improvement F: Batch Evaluation API ✅ - -**Current problem:** Each `eval` task evaluates a single point `x` in its own -worker process. For every evaluation: - -- One process slot is occupied by one objective call -- Process-spawn + IPC overhead is paid per point -- If `fun` supports batch input (as it does in SpotOptim's convention - `fun(X)` with `X` of shape `(n, d)`), the vectorization is never exploited - in parallel mode - -When the objective is cheap relative to process overhead (e.g. a fast -simulation that runs in < 1 second), single-point dispatch is inefficient. - -**Change:** Accumulate multiple candidate points from completed `search` tasks -and dispatch them as a single batch `eval` task when enough candidates are -available: - -```python -BATCH_SIZE = n_jobs # or a new `eval_batch_size` parameter - -pending_cands = [] # candidates awaiting evaluation - -# When a search task completes: -pending_cands.append(x_cand) - -if len(pending_cands) >= BATCH_SIZE or no_active_searches(): - X_batch = np.vstack(pending_cands) - pending_cands.clear() - fut = eval_pool.submit(remote_batch_eval_wrapper, dill.dumps((self, X_batch))) -``` - -The batch eval wrapper unpacks and calls `fun(X_batch)` once, returning -`(X_batch, y_batch)`. - -**Expected benefit:** -- Amortizes process-spawn + IPC overhead across `BATCH_SIZE` evaluations -- Allows user functions that are naturally vectorized to exploit that property -- Reduces total number of `dill.dumps` calls by factor `BATCH_SIZE` - -**New parameter:** `eval_batch_size: int = 1` (default preserves current -behaviour; set to `n_jobs` or higher to activate batching). - -**Compatibility:** Default `eval_batch_size=1` is fully backward compatible. -Users with vectorized objective functions explicitly opt in. - -**Risk:** Low. Batch dispatch slightly increases the latency before the first -result of a batch is available, which can temporarily reduce steady-state -throughput when `fun` is very slow. The configurable `eval_batch_size` -parameter lets users tune the trade-off. - ---- - -### Release 0.10.0 — Improvement D: Free-Threaded (No-GIL) Awareness ✅ - -**Background:** Python 3.13 introduced experimental free-threaded builds -(`python3.13t`, compile flag `--disable-gil`). When the GIL is disabled, -`ThreadPoolExecutor` achieves true CPU-level parallelism for any Python code, -not just GIL-releasing extensions. - -**Dependency:** This improvement extends Improvement C (ThreadPoolExecutor for -search tasks). With the GIL disabled, the `ThreadPoolExecutor` that already -runs search tasks gains full multi-core parallelism for any Python-level -computation inside `suggest_next_infill_point()` — no code change is required -for that path. - -**Change:** Added GIL-status detection via a module-level helper and -`contextlib.ExitStack`-based executor selection in `optimize_steady_state`: - -```python -import sys - -def _is_gil_disabled() -> bool: - """Return True if running on a free-threaded Python build.""" - return not getattr(sys, "_is_gil_enabled", lambda: True)() -``` - -Executor selection inside `optimize_steady_state`: - -```python -from contextlib import ExitStack - -_no_gil = _is_gil_disabled() - -with ExitStack() as _stack: - eval_pool = _stack.enter_context( - ThreadPoolExecutor(max_workers=self.n_jobs) - if _no_gil - else ProcessPoolExecutor(max_workers=self.n_jobs) - ) - search_pool = _stack.enter_context( - ThreadPoolExecutor(max_workers=self.n_jobs) - ) -``` - -When `_no_gil` is `True`, the `dill` serialization path for eval tasks is -eliminated. Phase 1 (initial design) calls `fun` directly via -`_thread_eval_task_single`; Phase 2 (batch eval) calls `fun` directly via -`_thread_batch_eval_task` — both closures access `self.evaluate_function()` -from the shared heap with no IPC. - -**Compatibility:** Purely additive. On standard GIL-enabled Python the -behavior is identical to 0.9.0 — `_no_gil` is `False` and the -`ProcessPoolExecutor` + `dill` path is taken unchanged. - -**Risk:** Low. The free-threaded build is opt-in at the Python level. -spotoptim does not force users onto it; it exploits it when present. -The `_surrogate_lock` remains in use on both paths. - ---- - -## 6. Summary - -| Release | Improvement | Key Change | Risk | Status | -|---------|-------------|------------|------|--------| -| **0.7.0** | E — `n_jobs=-1` | Resolve `-1` to `os.cpu_count()` | Minimal | ✅ Done | -| **0.8.0** | C — Threads for search | `ThreadPoolExecutor` for `suggest_next_infill_point`; `ProcessPoolExecutor` kept for `eval`; `threading.Lock` guards surrogate | Low | ✅ Done | -| **0.9.0** | F — Batch eval | `eval_batch_size` param; accumulate candidates; single `remote_batch_eval_wrapper` call per batch; one surrogate refit per batch | Low | ✅ Done | -| **0.10.0** | D — No-GIL | Detect `sys._is_gil_enabled()`; use threads for eval too | Low | Planned | - -Improvements A (shared memory) and B (SciPy `workers=`) are **not planned** -because A is superseded by C and B conflicts with the existing `vectorized=True` -optimisation. diff --git a/REUSE.toml b/REUSE.toml index d75ef420..4efe8f61 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -5,7 +5,6 @@ SPDX-PackageDownloadLocation = "https://github.com/sequential-parameter-optimiza [[annotations]] path = [ - ".flake8", ".gitignore", ".python-version", ".releaserc.json", diff --git a/_freeze/docs/early-stopping/execute-results/html.json b/_freeze/docs/early-stopping/execute-results/html.json index 84ba0b01..0f668431 100644 --- a/_freeze/docs/early-stopping/execute-results/html.json +++ b/_freeze/docs/early-stopping/execute-results/html.json @@ -1,10 +1,10 @@ { - "hash": "9201d6237924b1b5944f141ecabb4726", + "hash": "c32cf4f174cc879d0d88c70b1ba9db0c", "result": { "engine": "jupyter", - "markdown": "---\ntitle: \"Early Stopping with `max_restarts`\"\ndescription: \"Patience-based early-stopping rule for spotoptim. Terminate the outer restart loop after N consecutive restarts without improvement to best_y_, saving evaluation budget when the optimizer has stopped making progress.\"\n---\n\n`SpotOptim` has two hard termination conditions — `max_iter` (evaluation\nbudget) and `max_time` (wall clock). When the success-rate drops to zero for\n`restart_after_n` iterations, the optimizer **restarts** (fresh initial\ndesign, best-so-far injected). But restarting itself can plateau: the\noptimizer may resample similar regions over and over without ever improving\nthe incumbent. The `max_restarts` parameter adds a patience rule on top of\nthe existing restart machinery: after $N$ consecutive restarts without any\nimprovement to `best_y_`, the run terminates cleanly.\n\nThis chapter explains when to use `max_restarts`, how it interacts with the\nother stopping knobs, and shows an executable example on the built-in\n`sphere` function.\n\n::: {.callout-note}\n### How this compares to competitor toolkits\n\n| Toolkit | Name of equivalent mechanism | Notes |\n| :-- | :-- | :-- |\n| Hyperopt | [`no_progress_loss(iteration_stop_count=N)`](https://github.com/hyperopt/hyperopt/blob/master/hyperopt/early_stop.py) | Stops after N trials without improvement to the best loss. |\n| Ray Tune | [`ExperimentPlateauStopper`](https://docs.ray.io/en/latest/tune/api/doc/ray.tune.stopper.ExperimentPlateauStopper.html) | Stops when the standard deviation of the top results is below a threshold. |\n| SMAC3 | [`scenario.terminate_cost_threshold`](https://automl.github.io/SMAC3/) | Stops when a target cost is reached — a closely related absolute-value rule. |\n| spotoptim | `max_restarts` | Patience counted at the **restart** level, not the iteration level — reuses the existing success-rate signal. |\n\nThe `max_restarts` rule deliberately counts at the *restart* level. The\nsuccess-rate + restart machinery already embodies the \"local search has\nstalled\" signal; an iteration-level patience would just duplicate it.\n:::\n\n## When to enable `max_restarts`\n\nEnable `max_restarts` when you want the run to end *early* once the\noptimizer has clearly plateaued — for example:\n\n* Hyperparameter sweeps where a long idle tail would waste compute.\n* Noisy objectives where a single unlucky restart might not justify\n doubling the budget.\n* Reproducible benchmarks where you want the run length to be\n outcome-dependent rather than budget-dependent.\n\nLeave `max_restarts` at its default `None` (unlimited restarts) when you\nwant the legacy behaviour: run until `max_iter` or `max_time` triggers.\nThe default preserves byte-for-byte compatibility with runs created before\nthe feature existed.\n\n::: {.callout-tip}\n### Choosing `max_restarts`\n\nA good starting point is `max_restarts=2` or `3`, paired with a moderate\n`restart_after_n` (e.g. `3`) and `window_size` (e.g. `3`). Two wasted\nrestarts is usually enough evidence that the surrogate has nothing useful\nleft to exploit. For strictly bounded ceilings — \"never do more than five\nrestarts total\" — set `max_restarts=5` directly; the rule acts as a hard\ncap on total restarts because any non-improving restart increments the\ncounter.\n\n`max_restarts=0` is the strictest setting: the very first restart that\nfails to improve the incumbent terminates the run. Use this as a\none-chance gate for expensive objectives.\n:::\n\n## Minimal working example\n\nThe example uses the 2-D `sphere` function with a configuration that is\nguaranteed to trigger early stopping quickly. The objective is simple\nenough that LHS plus a single surrogate round usually lands on the minimum,\nso any subsequent restart cannot improve it.\n\n::: {#5b145cf2 .cell execution_count=1}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=200, # generous budget — should NOT be exhausted\n n_initial=5,\n restart_after_n=3, # trigger a restart after 3 stalled iterations\n window_size=3, # window for the success-rate signal\n max_restarts=2, # stop after 2 consecutive fruitless restarts\n seed=0,\n verbose=False,\n)\nresult = opt.optimize()\n\nprint(result.message.splitlines()[0])\nprint(f\"Evaluations used: {result.nfev}\")\nprint(f\"Best objective : {result.fun:.6g}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nOptimization early stopped: no improvement for 2 consecutive restarts\nEvaluations used: 20\nBest objective : 4.19661e-07\n```\n:::\n:::\n\n\nThe resulting `OptimizeResult` has:\n\n* `success=True` — plateau-termination is a *graceful* outcome. `False` is\n reserved for hard failures (NaN/inf loops, surrogate fit errors, …).\n This convention matches Ray Tune and SMAC.\n* `message` starts with `\"Optimization early stopped: no improvement for\n N consecutive restarts\"`, letting downstream pipelines distinguish early\n stop from budget exhaustion with a string check.\n* `nfev < max_iter` — the evaluation budget was *not* exhausted.\n\n## Programmatic inspection\n\nAfter the run, the private attribute `opt._early_stopped` is `True` iff\nearly stopping fired, and `opt.restarts_results_` lists one\n`OptimizeResult` per restart:\n\n::: {#944793bb .cell execution_count=2}\n``` {.python .cell-code}\nprint(f\"Early-stopped : {opt._early_stopped}\")\nprint(f\"Total restarts : {len(opt.restarts_results_)}\")\nprint(f\"Best fun per restart: {[round(r.fun, 6) for r in opt.restarts_results_]}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nEarly-stopped : True\nTotal restarts : 3\nBest fun per restart: [np.float64(0.0), np.float64(0.0), np.float64(0.0)]\n```\n:::\n:::\n\n\n## Interaction with `max_iter` and `max_time`\n\nThe three termination rules are **all active simultaneously**. Whichever\ntriggers first wins:\n\n| Rule | Triggered when | `success` | Typical message prefix |\n| :-- | :-- | :-- | :-- |\n| `max_iter` | `len(opt.y_) >= max_iter` | `True` | \"Optimization terminated: reached max iterations\" |\n| `max_time` | `time.time() - t_start >= max_time` | `True` | \"Optimization terminated: reached max time\" |\n| `max_restarts` | $N$ consecutive restarts with no improvement | `True` | \"Optimization early stopped: no improvement for $N$ consecutive restarts\" |\n\n`max_restarts` never *replaces* the other two — it only adds an\nearlier off-ramp. If you give the optimizer a tiny budget that cannot even\nreach `restart_after_n + 1` iterations, `max_iter` will terminate the run\nand `max_restarts` will never fire.\n\n::: {.callout-warning}\n### `max_restarts=0` does not disable the rule\n\n`max_restarts=None` disables the rule. `max_restarts=0` is the *strictest*\nsetting: stop on the first non-improving restart. This mirrors how\nHyperopt's `no_progress_loss(0)` behaves — zero means \"zero tolerance\".\nIf you want to run without early stopping, pass `None` or omit the\nargument.\n:::\n\n## Parameter reference\n\n| Parameter | Default | Purpose |\n| :-- | :-- | :-- |\n| `max_restarts` | `None` | Stop after this many consecutive fruitless restarts. `None` = unlimited. |\n| `restart_after_n` | `3` | Number of iterations with zero success rate before a restart is attempted. |\n| `window_size` | `3` | Sliding-window width used by the success-rate statistic. |\n| `restart_inject_best` | `True` | Whether the incumbent is seeded into the initial design of each restart. |\n| `max_iter` | `20` | Evaluation budget (counts initial design + infill). |\n| `max_time` | `inf` | Wall-clock limit in seconds. |\n\nAll of these live on `SpotOptimConfig` and can be passed as keyword\narguments to the `SpotOptim(...)` constructor.\n\n## Future work — pluggable stopping criteria\n\n`max_restarts` is the first step of a broader roadmap. Planned phases:\n\n* **Phase 2** — pluggable `StoppingCriterion` protocol with built-in\n `TargetValueStopper` (absolute fvalue threshold, mirroring SMAC's\n `terminate_cost_threshold`), `ExpectedImprovementStopper` (based on\n Makarova et al. 2022, [arxiv.org/abs/2104.08166](https://arxiv.org/abs/2104.08166)),\n and `PlateauStopper` (standard-deviation window, mirroring Ray Tune's\n `ExperimentPlateauStopper`). A user callback hook\n `early_stop_fn: Callable[[SpotOptim], tuple[bool, str]]` will mirror\n Hyperopt's `fmin(..., early_stop_fn=...)`.\n* **Phase 3** — research-grade log-EI convergence criterion with\n theoretical guarantees (BoTorch community direction).\n\nOut of scope: multi-fidelity schedulers (Hyperband / BOHB successive\nhalving) and bandit-style pruners (Optuna `HyperbandPruner`,\n`MedianPruner`). These are architectural initiatives, not early-stopping\nfeatures — they prune *inside* a multi-trial ML training run, whereas\n`spotoptim`'s unit of work is a single function evaluation.\n\n## See also\n\n* [Sequential Optimization](optimize_seq.qmd) — outer restart loop and\n `execute_optimization_run()`.\n* [Parallel Optimization](optimize_parallel.qmd) — `max_restarts` fires\n identically under `n_jobs>1` steady-state parallelism.\n* [Running on Slurm (GWDG NHR)](slurm.qmd) — bake `max_restarts` into the\n experiment pickle to avoid re-dispatching a job once the optimizer has\n plateaued.\n* `SpotOptim.SpotOptimConfig` in the API reference — the authoritative\n parameter list.\n\n", + "markdown": "---\ntitle: \"Early Stopping with `max_restarts`\"\ndescription: \"Patience-based early-stopping rule for spotoptim. Terminate the outer restart loop after N consecutive restarts without improvement to best_y_, saving evaluation budget when the optimizer has stopped making progress.\"\n---\n\n`SpotOptim` has two hard termination conditions — `max_iter` (evaluation\nbudget) and `max_time` (wall clock). When the success-rate drops to zero for\n`restart_after_n` iterations, the optimizer **restarts** (fresh initial\ndesign, best-so-far injected). But restarting itself can plateau: the\noptimizer may resample similar regions over and over without ever improving\nthe incumbent. The `max_restarts` parameter adds a patience rule on top of\nthe existing restart machinery: after $N$ consecutive restarts without any\nimprovement to `best_y_`, the run terminates cleanly.\n\nThis chapter explains when to use `max_restarts`, how it interacts with the\nother stopping knobs, and shows an executable example on the built-in\n`sphere` function.\n\n::: {.callout-note}\n### How this compares to competitor toolkits\n\n| Toolkit | Name of equivalent mechanism | Notes |\n| :-- | :-- | :-- |\n| Hyperopt | [`no_progress_loss(iteration_stop_count=N)`](https://github.com/hyperopt/hyperopt/blob/master/hyperopt/early_stop.py) | Stops after N trials without improvement to the best loss. |\n| Ray Tune | [`ExperimentPlateauStopper`](https://docs.ray.io/en/latest/tune/api/doc/ray.tune.stopper.ExperimentPlateauStopper.html) | Stops when the standard deviation of the top results is below a threshold. |\n| SMAC3 | [`scenario.terminate_cost_threshold`](https://automl.github.io/SMAC3/) | Stops when a target cost is reached — a closely related absolute-value rule. |\n| spotoptim | `max_restarts` | Patience counted at the **restart** level, not the iteration level — reuses the existing success-rate signal. |\n\nThe `max_restarts` rule deliberately counts at the *restart* level. The\nsuccess-rate + restart machinery already embodies the \"local search has\nstalled\" signal; an iteration-level patience would just duplicate it.\n:::\n\n## When to enable `max_restarts`\n\nEnable `max_restarts` when you want the run to end *early* once the\noptimizer has clearly plateaued — for example:\n\n* Hyperparameter sweeps where a long idle tail would waste compute.\n* Noisy objectives where a single unlucky restart might not justify\n doubling the budget.\n* Reproducible benchmarks where you want the run length to be\n outcome-dependent rather than budget-dependent.\n\nLeave `max_restarts` at its default `None` (unlimited restarts) when you\nwant the legacy behaviour: run until `max_iter` or `max_time` triggers.\nThe default preserves byte-for-byte compatibility with runs created before\nthe feature existed.\n\n::: {.callout-tip}\n### Choosing `max_restarts`\n\nA good starting point is `max_restarts=2` or `3`, paired with a moderate\n`restart_after_n` (e.g. `3`) and `window_size` (e.g. `3`). Two wasted\nrestarts is usually enough evidence that the surrogate has nothing useful\nleft to exploit. For strictly bounded ceilings — \"never do more than five\nrestarts total\" — set `max_restarts=5` directly; the rule acts as a hard\ncap on total restarts because any non-improving restart increments the\ncounter.\n\n`max_restarts=0` is the strictest setting: the very first restart that\nfails to improve the incumbent terminates the run. Use this as a\none-chance gate for expensive objectives.\n:::\n\n## Minimal working example\n\nThe example uses the 2-D `sphere` function with a configuration that is\nguaranteed to trigger early stopping quickly. The objective is simple\nenough that LHS plus a single surrogate round usually lands on the minimum,\nso any subsequent restart cannot improve it.\n\n::: {#2079d857 .cell execution_count=1}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=200, # generous budget — should NOT be exhausted\n n_initial=5,\n restart_after_n=3, # trigger a restart after 3 stalled iterations\n window_size=3, # window for the success-rate signal\n max_restarts=2, # stop after 2 consecutive fruitless restarts\n seed=0,\n verbose=False,\n)\nresult = opt.optimize()\n\nprint(result.message.splitlines()[0])\nprint(f\"Evaluations used: {result.nfev}\")\nprint(f\"Best objective : {result.fun:.6g}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nOptimization early stopped: no improvement for 2 consecutive restarts\nEvaluations used: 20\nBest objective : 4.19661e-07\n```\n:::\n:::\n\n\nThe resulting `OptimizeResult` has:\n\n* `success=True` — plateau-termination is a *graceful* outcome. `False` is\n reserved for hard failures (NaN/inf loops, surrogate fit errors, …).\n This convention matches Ray Tune and SMAC.\n* `message` starts with `\"Optimization early stopped: no improvement for\n N consecutive restarts\"`, letting downstream pipelines distinguish early\n stop from budget exhaustion with a string check.\n* `nfev < max_iter` — the evaluation budget was *not* exhausted.\n\n## Programmatic inspection\n\nAfter the run, the private attribute `opt._early_stopped` is `True` iff\nearly stopping fired, and `opt.restarts_results_` lists one\n`OptimizeResult` per restart:\n\n::: {#4861a0aa .cell execution_count=2}\n``` {.python .cell-code}\nprint(f\"Early-stopped : {opt._early_stopped}\")\nprint(f\"Total restarts : {len(opt.restarts_results_)}\")\nprint(f\"Best fun per restart: {[round(r.fun, 6) for r in opt.restarts_results_]}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nEarly-stopped : True\nTotal restarts : 3\nBest fun per restart: [np.float64(0.0), np.float64(0.0), np.float64(0.0)]\n```\n:::\n:::\n\n\n## Interaction with `max_iter` and `max_time`\n\nThe three termination rules are **all active simultaneously**. Whichever\ntriggers first wins:\n\n| Rule | Triggered when | `success` | Typical message prefix |\n| :-- | :-- | :-- | :-- |\n| `max_iter` | `len(opt.y_) >= max_iter` | `True` | \"Optimization terminated: reached max iterations\" |\n| `max_time` | `time.time() - t_start >= max_time` | `True` | \"Optimization terminated: reached max time\" |\n| `max_restarts` | $N$ consecutive restarts with no improvement | `True` | \"Optimization early stopped: no improvement for $N$ consecutive restarts\" |\n\n`max_restarts` never *replaces* the other two — it only adds an\nearlier off-ramp. If you give the optimizer a tiny budget that cannot even\nreach `restart_after_n + 1` iterations, `max_iter` will terminate the run\nand `max_restarts` will never fire.\n\n::: {.callout-warning}\n### `max_restarts=0` does not disable the rule\n\n`max_restarts=None` disables the rule. `max_restarts=0` is the *strictest*\nsetting: stop on the first non-improving restart. This mirrors how\nHyperopt's `no_progress_loss(0)` behaves — zero means \"zero tolerance\".\nIf you want to run without early stopping, pass `None` or omit the\nargument.\n:::\n\n## Parameter reference\n\n| Parameter | Default | Purpose |\n| :-- | :-- | :-- |\n| `max_restarts` | `None` | Stop after this many consecutive fruitless restarts. `None` = unlimited. |\n| `restart_after_n` | `3` | Number of iterations with zero success rate before a restart is attempted. |\n| `window_size` | `3` | Sliding-window width used by the success-rate statistic. |\n| `restart_inject_best` | `True` | Whether the incumbent is seeded into the initial design of each restart. |\n| `max_iter` | `20` | Evaluation budget (counts initial design + infill). |\n| `max_time` | `inf` | Wall-clock limit in seconds. |\n\nAll of these live on `SpotOptimConfig` and can be passed as keyword\narguments to the `SpotOptim(...)` constructor.\n\n## Future work — pluggable stopping criteria\n\n`max_restarts` is the first step of a broader roadmap. Planned phases:\n\n* **Phase 2** — pluggable `StoppingCriterion` protocol with built-in\n `TargetValueStopper` (absolute fvalue threshold, mirroring SMAC's\n `terminate_cost_threshold`), `ExpectedImprovementStopper` (based on\n Makarova et al. 2022, [arxiv.org/abs/2104.08166](https://arxiv.org/abs/2104.08166)),\n and `PlateauStopper` (standard-deviation window, mirroring Ray Tune's\n `ExperimentPlateauStopper`). A user callback hook\n `early_stop_fn: Callable[[SpotOptim], tuple[bool, str]]` will mirror\n Hyperopt's `fmin(..., early_stop_fn=...)`.\n* **Phase 3** — research-grade log-EI convergence criterion with\n theoretical guarantees (BoTorch community direction).\n\nOut of scope: multi-fidelity schedulers (Hyperband / BOHB successive\nhalving) and bandit-style pruners (Optuna `HyperbandPruner`,\n`MedianPruner`). These are architectural initiatives, not early-stopping\nfeatures — they prune *inside* a multi-trial ML training run, whereas\n`spotoptim`'s unit of work is a single function evaluation.\n\n## See also\n\n* [Sequential Optimization](optimize_seq.qmd) — outer restart loop and\n `execute_optimization_run()`.\n* `SpotOptim.SpotOptimConfig` in the API reference — the authoritative\n parameter list.\n\n", "supporting": [ - "early-stopping_files" + "early-stopping_files/figure-html" ], "filters": [], "includes": {} diff --git a/_freeze/docs/optimize_parallel/execute-results/html.json b/_freeze/docs/optimize_parallel/execute-results/html.json deleted file mode 100644 index ee80676b..00000000 --- a/_freeze/docs/optimize_parallel/execute-results/html.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "hash": "b1a0261272104fee0dd873ffa7c973da", - "result": { - "engine": "jupyter", - "markdown": "---\ntitle: \"SpotOptim: Parallel Optimization\"\ndescription: \"Step-by-step walkthrough of execute_optimization_run() and every method it calls along the parallel (steady-state) path, with executable examples validated by pytest.\"\n---\n\nThis document traces every step executed by `SpotOptim.execute_optimization_run()` along\nthe parallel code path (`n_jobs > 1`), in the order they occur.\nEach section describes one method or phase with a `{python}` code block that can be executed directly.\n\nThe public entry point is `optimize()`, which manages the outer restart loop and delegates\neach cycle to `execute_optimization_run()`.\nWhen `n_jobs > 1`, that dispatcher routes to `optimize_steady_state()`,\nwhich implements a hybrid steady-state parallelisation strategy that overlaps surrogate\nsearch with objective function evaluation.\nThis document covers that path in full.\n\nRun all related tests with:\n\n```bash\nuv run pytest tests/test_spotoptim_deep.py -v\n```\n\n---\n\n## Step 1 — Dispatch (`execute_optimization_run()`)\n\n```python\nif self.n_jobs > 1:\n return self.optimize_steady_state(...)\nelse:\n return self.optimize_sequential_run(...)\n```\n\n`execute_optimization_run()` is the routing layer between the outer restart loop in\n`optimize()` and the actual optimisation engine.\nIts sole responsibility is to examine `n_jobs` and forward all arguments to either\n`optimize_steady_state()` (parallel) or `optimize_sequential_run()` (sequential).\nIt returns a `(status, OptimizeResult)` tuple in both cases, which `optimize()` uses to\ndecide whether to restart or terminate.\nThe optional `shared_best_y` and `shared_lock` parameters support inter-worker\ncoordination; they are unused in the current steady-state implementation and reserved\nfor future multi-restart parallelism.\n\n::: {#81dbf60f .cell execution_count=1}\n``` {.python .cell-code}\nimport time\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, seed=0, n_jobs=2)\nstatus, result = opt.execute_optimization_run(timeout_start=time.time())\nprint(f\"status : {status}\")\nprint(f\"best : {result.fun:.6f}\")\nassert status == \"FINISHED\"\nprint(\"dispatch check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nstatus : FINISHED\nbest : 1.106399\ndispatch check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 2 — Parallel Run Orchestration (`optimize_steady_state()`)\n\n```python\nself.set_seed()\nX0 = self.get_initial_design(X0)\nX0 = self.curate_initial_design(X0)\n# Phase 1: parallel initial evaluation\n# Phase 2: steady-state loop\nreturn \"FINISHED\", OptimizeResult(...)\n```\n\n`optimize_steady_state()` is the parallel orchestrator.\nIt follows the same preparatory sequence as its sequential counterpart — seeding the\nrandom number generator, generating and curating the initial design — before switching\nto a pool-based execution model.\nThe method never returns `\"RESTART\"`: unlike the sequential loop, the steady-state engine\ndoes not implement a zero-success-rate restart criterion, and always exits with\n`\"FINISHED\"`.\nAll worker pools are managed inside a single `contextlib.ExitStack` that guarantees\norderly shutdown on success and on exception.\n\n::: {#ad3227ca .cell execution_count=2}\n``` {.python .cell-code}\nimport time\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, seed=0, n_jobs=2)\nstatus, result = opt.optimize_steady_state(timeout_start=time.time(), X0=None)\nprint(f\"status : {status}\")\nprint(f\"evaluations : {result.nfev}\")\nprint(f\"best : {result.fun:.6f}\")\nassert status == \"FINISHED\"\nprint(\"parallel orchestration check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nstatus : FINISHED\nevaluations : 10\nbest : 1.106399\nparallel orchestration check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 3 — Seed and Initial Design (`set_seed()`, `get_initial_design()`, `curate_initial_design()`)\n\n```python\nself.set_seed()\nX0 = self.get_initial_design(X0)\nX0 = self.curate_initial_design(X0)\n```\n\nThe parallel path begins with the same three preparatory calls as the sequential path.\n`set_seed()` re-seeds Python's `random` module and NumPy's global generator to ensure\nreproducibility.\n`get_initial_design()` either processes the user-supplied `X0` or generates a Latin\nHypercube sample in the transformed, reduced search space.\n`curate_initial_design()` removes duplicate points and generates replacements as needed.\nBecause points are dispatched concurrently to worker processes or threads in the next\nphase, curation must be complete before submission: worker processes operate on a\nsnapshot of the optimiser serialised with `dill` and cannot interact with the\nmain-process state.\nImmediately after curation, restart injection is applied: a `y0_prefilled` array (all\n`NaN` by default) is populated with `y0_known` at the position of the matching `self.x0`\npoint, using the same distance tolerance as `_initialize_run()` in the sequential path.\nThis pre-filled array is consumed by Phase 1 to skip one pool submission per restart.\n\n::: {#9add94ae .cell execution_count=3}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], n_initial=5, seed=42, n_jobs=2)\nopt.set_seed()\nX0 = opt.get_initial_design(None)\nX0 = opt.curate_initial_design(X0)\nprint(f\"initial design shape : {X0.shape}\")\nassert X0.shape == (5, 2)\nassert len(np.unique(X0, axis=0)) == 5\nprint(\"seed and initial design check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ninitial design shape : (5, 2)\nseed and initial design check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 4 — GIL Detection and Executor Construction (`is_gil_disabled()`)\n\n```python\n_no_gil = is_gil_disabled()\nwith ExitStack() as _stack:\n eval_pool = _stack.enter_context(\n ThreadPoolExecutor(max_workers=self.n_jobs) if _no_gil\n else ProcessPoolExecutor(max_workers=self.n_jobs)\n )\n search_pool = _stack.enter_context(\n ThreadPoolExecutor(max_workers=self.n_jobs)\n )\n```\n\n`is_gil_disabled()` queries `sys._is_gil_enabled()` (Python 3.13+) to detect whether\nthe interpreter was built without the Global Interpreter Lock.\nOn standard GIL builds — Python 3.12 or GIL-enabled 3.13 — objective evaluations are\ndispatched to a `ProcessPoolExecutor` so that each worker runs in a separate process,\ngiving true CPU-level parallelism and safe isolation of arbitrary callables.\nSurrogate search tasks are always dispatched to a `ThreadPoolExecutor` because they\nshare the main-process heap and require no serialisation.\nOn free-threaded builds (`python3.13t`) both pools are `ThreadPoolExecutor` instances:\nthreads achieve true parallelism without the GIL, `dill` serialisation is eliminated,\nand the objective function is called directly from the shared heap.\nThe `_surrogate_lock` (a `threading.Lock`) is used in both configurations to serialise\nconcurrent surrogate reads and refits.\n\n::: {#675dc687 .cell execution_count=4}\n``` {.python .cell-code}\nfrom spotoptim.utils.parallel import is_gil_disabled\n\nresult = is_gil_disabled()\nprint(f\"GIL disabled: {result}\")\nassert isinstance(result, bool)\nprint(\"GIL detection check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nGIL disabled: False\nGIL detection check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 5 — Phase 1: Parallel Initial Submission\n\n```python\nn_to_submit = 0\nfor i, x in enumerate(X0):\n if np.isfinite(y0_prefilled[i]):\n # Restart injection: store directly, skip the pool.\n self._update_storage_steady(x, y0_prefilled[i])\n continue\n if _no_gil:\n fut = eval_pool.submit(_thread_eval_task_single, x)\n else:\n pickled_args = dill.dumps((self, x))\n fut = eval_pool.submit(remote_eval_wrapper, pickled_args)\n futures[fut] = \"eval\"\n n_to_submit += 1\n```\n\nThe initial design is partitioned into two groups before any worker is touched.\nPoints that carry a pre-filled `y0_prefilled` value — set during Step 4 for the\nrestart-injected best point — are stored on the main thread via\n`_update_storage_steady()` and skipped entirely.\nThe remaining `n_to_submit` points are submitted to `eval_pool` concurrently.\nOn GIL builds each point is serialised together with a snapshot of the optimiser using\n`dill.dumps()`; the TensorBoard writer is temporarily set to `None` before serialisation\nbecause `SummaryWriter` objects are not picklable.\n`remote_eval_wrapper()` unpickles the arguments in the worker process, reshapes the\npoint to a `(1, d)` array, calls `evaluate_function()`, and returns the `(x, y)` pair.\nOn free-threaded builds `_thread_eval_task_single()` calls `evaluate_function()` without\nany serialisation overhead.\nAll submitted futures are tracked in a `futures` dictionary keyed by `Future` object\nwith the string tag `\"eval\"`.\nWhen `y0_known` is `None` (no restart), `y0_prefilled` is all-NaN and `n_to_submit`\nequals `n_initial`, so behaviour is identical to a fresh run.\n\n::: {#4d9eacf8 .cell execution_count=5}\n``` {.python .cell-code}\nimport time\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\n# Fresh run — all n_initial points are submitted to the pool.\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=8, max_iter=8, seed=0, n_jobs=2)\nresult = opt.optimize()\nassert result.nfev == 8\nprint(f\"evaluations (no injection): {result.nfev}\")\n\n# Restart injection — the known best point is stored directly, not re-evaluated.\nx_inject = np.array([0.5, -0.5])\nopt2 = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=6, max_iter=6, seed=1, n_jobs=2,\n x0=x_inject)\nopt2.optimize_steady_state(\n timeout_start=time.time(),\n X0=None,\n y0_known=float(np.sum(x_inject**2)),\n)\nassert float(np.sum(x_inject**2)) in opt2.y_\nprint(f\"injected value present in y_: {float(np.sum(x_inject**2)):.4f}\")\nprint(\"parallel initial submission check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nevaluations (no injection): 8\ninjected value present in y_: 0.5000\nparallel initial submission check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 6 — Initial Design Collection (`_update_storage_steady()`)\n\n```python\nwhile initial_done_count < len(X0):\n done, _ = wait(futures.keys(), return_when=FIRST_COMPLETED)\n for fut in done:\n ftype = futures.pop(fut)\n if ftype != \"eval\":\n continue\n x_done, y_done = fut.result()\n if not isinstance(y_done, Exception):\n self._update_storage_steady(x_done, y_done)\n initial_done_count += 1\n```\n\nThe main thread waits in a loop using `concurrent.futures.wait()` with\n`return_when=FIRST_COMPLETED`, processing each completed future as it arrives.\nFutures tagged `\"eval\"` are the only type active during Phase 1; any unexpected type\nis silently skipped.\nWhen a result is an `Exception` instance — indicating a worker-side failure — the point\nis dropped and, if `verbose=True`, the error is printed; the initial design count still\nadvances so the loop terminates correctly.\nFor valid results, `_update_storage_steady()` appends the point in original scale to\n`X_` and its objective value to `y_`, initialising both arrays on the first call.\nIt also updates `best_x_`, `best_y_`, `min_y`, and `min_X` in-place whenever the new\nvalue improves on the current best.\nBecause the main thread is the only writer during Phase 1, no locking is required here.\n\n::: {#dcaa7bcf .cell execution_count=6}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=5, seed=0, n_jobs=2)\nopt.optimize()\nprint(f\"X_ shape : {opt.X_.shape}\")\nprint(f\"y_ : {opt.y_}\")\nassert opt.X_.shape[0] >= 1\nassert np.isfinite(opt.best_y_)\nprint(\"_update_storage_steady check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nX_ shape : (5, 2)\ny_ : [ 8.46320329 30.97516031 16.65114474 19.210481 9.97487171]\n_update_storage_steady check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 7 — Initial Postprocessing (`_init_tensorboard()`, `update_stats()`, `get_best_xy_initial_design()`)\n\n```python\nself._init_tensorboard()\nif self.y_ is None or len(self.y_) == 0:\n raise RuntimeError(...)\nself.update_stats()\nself.get_best_xy_initial_design()\n```\n\nOnce all initial evaluations have completed, three postprocessing steps are applied in\nfixed order.\n`_init_tensorboard()` logs each initial-design point to TensorBoard as a separate\nhyperparameter run; when `tensorboard_log=False` (the default) it is a no-op.\nA guard check follows: if `y_` is `None` or empty, every worker evaluation failed and\na `RuntimeError` is raised with a diagnostic message pointing to likely causes such as\nunpicklable callables or missing imports inside the worker process.\n`update_stats()` refreshes `min_y`, `min_X`, `counter`, and, for noisy objectives,\naggregated per-point means and variances.\n`get_best_xy_initial_design()` identifies the initial best solution and writes it to\n`best_x_` and `best_y_`; unlike the sequential path, these attributes are updated\nincrementally by `_update_storage_steady()` throughout the loop, so this call aligns\nthe verbose best-solution display with the true running best after Phase 1.\n\n::: {#6c8536a1 .cell execution_count=7}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)], tensorboard_log=False,\n n_initial=5, max_iter=5, seed=0, n_jobs=2)\nopt.optimize()\nassert opt.tb_writer is None\nassert opt.counter == 5\nprint(f\"counter : {opt.counter}\")\nprint(f\"min_y : {opt.min_y:.6f}\")\nprint(\"initial postprocessing check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ncounter : 5\nmin_y : 0.560692\ninitial postprocessing check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 8 — First Surrogate Fit (`fit_scheduler()`)\n\n```python\n# No lock needed — no search threads active yet\nself.fit_scheduler()\n```\n\nAfter the initial postprocessing, the surrogate model is fitted to the complete initial\ndesign for the first time.\nNo surrogate lock is acquired here because the `search_pool` has not yet been populated:\nthis is the only point in the parallel path where `fit_scheduler()` is called without\nholding `_surrogate_lock`.\n`fit_scheduler()` selects the most recent `window_size` training points according to\n`selection_method`, fits the surrogate, and prepares it for acquisition-function\nqueries.\nWhen a list of surrogates was specified at construction, one is chosen probabilistically\naccording to `prob_surrogate` before fitting.\n\n::: {#fea8aadf .cell execution_count=8}\n``` {.python .cell-code}\nimport time\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=15, window_size=10, seed=0, n_jobs=2)\nresult = opt.optimize()\nprint(f\"window_size : {opt.window_size}\")\nprint(f\"evaluations : {result.nfev}\")\nassert opt.window_size == 10\nprint(\"first surrogate fit check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nwindow_size : 10\nevaluations : 15\nfirst surrogate fit check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 9 — Steady-State Loop Overview (`optimize_steady_state()`)\n\n```python\npending_cands: list = []\n_future_n_pts: dict = {}\n\nwhile (len(self.y_) < effective_max_iter) and (\n time.time() < timeout_start + self.max_time * 60\n):\n if _batch_ready():\n _flush_batch()\n # fill open slots with search tasks\n # flush again if threshold crossed\n # wait for any future to complete\n # route result by ftype: \"search\" or \"batch_eval\"\n```\n\nThe main iteration loop runs until either the evaluation budget `effective_max_iter` or\nthe wall-clock limit `max_time` minutes is exhausted.\nTwo data structures coordinate the flow: `pending_cands` accumulates candidates returned\nby completed search tasks, and `_future_n_pts` maps each in-flight batch-eval future to\nthe number of points it carries.\nBoth structures are required for accurate budget accounting: the reserved count is\n`len(y_) + n_in_flight + n_active_searches + len(pending_cands)`, ensuring the total\nnever exceeds `effective_max_iter` regardless of concurrency.\nEach loop iteration performs four actions in order — batch flush, slot filling, second\nflush, and future wait — before routing completed results by their type tag.\n\n::: {#e1f1d07f .cell execution_count=9}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=20, seed=0, n_jobs=2)\nresult = opt.optimize()\nprint(f\"evaluations : {result.nfev}\")\nprint(f\"best : {result.fun:.6f}\")\nassert result.nfev <= 20\nassert result.success\nprint(\"steady-state loop check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nevaluations : 20\nbest : 0.000000\nsteady-state loop check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 10 — Batch Readiness (`_batch_ready()`)\n\n```python\ndef _batch_ready() -> bool:\n if not pending_cands:\n return False\n if len(pending_cands) >= self.eval_batch_size:\n return True\n return not any(t == \"search\" for t in futures.values())\n```\n\n`_batch_ready()` determines whether the accumulated candidates in `pending_cands` should\nbe dispatched as a batch evaluation.\nThe primary condition is that the number of pending candidates meets or exceeds\n`eval_batch_size`.\nA secondary starvation-guard condition also triggers a flush when no search tasks remain\nin flight: without this guard, pending candidates would block indefinitely if the budget\nis nearly exhausted and no further search tasks can be submitted.\nWith `eval_batch_size=1` (the default), `_batch_ready()` returns `True` whenever any\ncandidate is pending, preserving the one-point-at-a-time behaviour of the sequential\npath.\nLarger values of `eval_batch_size` amortise process-spawn and inter-process\ncommunication overhead across multiple points, improving throughput for expensive\nobjectives on GIL builds.\n\n::: {#9633c063 .cell execution_count=10}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\n# Default batch size = 1: each search result is dispatched immediately\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=15, seed=0, n_jobs=2, eval_batch_size=1)\nresult = opt.optimize()\nassert opt.eval_batch_size == 1\nprint(f\"eval_batch_size : {opt.eval_batch_size}\")\nprint(f\"evaluations : {result.nfev}\")\nprint(\"_batch_ready check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\neval_batch_size : 1\nevaluations : 15\n_batch_ready check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 11 — Batch Dispatch (`_flush_batch()`)\n\n```python\ndef _flush_batch() -> None:\n X_batch = np.vstack(pending_cands)\n n_in_batch = len(pending_cands)\n pending_cands.clear()\n if _no_gil:\n fut_eval = eval_pool.submit(_thread_batch_eval_task, X_batch)\n else:\n pickled_args = dill.dumps((self, X_batch))\n fut_eval = eval_pool.submit(remote_batch_eval_wrapper, pickled_args)\n futures[fut_eval] = \"batch_eval\"\n _future_n_pts[fut_eval] = n_in_batch\n```\n\n`_flush_batch()` stacks all pending candidates into a single `(n, d)` array `X_batch`,\nclears `pending_cands`, and dispatches the batch to `eval_pool` as one future.\nOn GIL builds `remote_batch_eval_wrapper()` is used: it unpickles the optimiser and\nbatch in the worker process, calls `evaluate_function(X_batch)` once, and returns\n`(X_batch, y_batch)`.\nEvaluating the whole batch in a single `fun()` call avoids repeated process-spawn\noverhead and allows vectorised objective implementations to exploit NumPy-level\nparallelism within the worker.\nOn free-threaded builds `_thread_batch_eval_task()` performs the same operation\ndirectly in a thread without serialisation.\nThe future is registered under the `\"batch_eval\"` tag and its point count is recorded in\n`_future_n_pts` so that budget accounting remains correct while the evaluation is\nin flight.\n\n::: {#86e0bb8d .cell execution_count=11}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=20, seed=0, n_jobs=2, eval_batch_size=3)\nresult = opt.optimize()\nassert opt.eval_batch_size == 3\nprint(f\"eval_batch_size : {opt.eval_batch_size}\")\nprint(f\"evaluations : {result.nfev}\")\nprint(\"_flush_batch check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\neval_batch_size : 3\nevaluations : 20\n_flush_batch check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 12 — Search Slot Management (`_thread_search_task()`)\n\n```python\nn_in_flight = sum(_future_n_pts.values())\nn_searches = sum(1 for t in futures.values() if t == \"search\")\nreserved = len(self.y_) + n_in_flight + n_searches + len(pending_cands)\nif reserved < effective_max_iter:\n fut = search_pool.submit(_thread_search_task)\n futures[fut] = \"search\"\n```\n\nAt each loop iteration, the main thread fills any open slots in the search pool up to\n`n_jobs` concurrent tasks.\nBefore submitting a new search task, the budget guard checks that adding one more\nin-flight search would not push the reserved count over `effective_max_iter`: if it\nwould, no further search tasks are submitted and the loop drains existing futures until\nthe budget is consumed.\n`_thread_search_task()` is a nested closure that acquires `_surrogate_lock` before\ncalling `suggest_next_infill_point()` and releases it on return, so that a concurrent\nsurrogate refit on the main thread cannot corrupt the model while a search thread reads\nit.\nBecause search tasks are always submitted to a `ThreadPoolExecutor`, they share the main\nprocess heap and require no serialisation regardless of the GIL state.\n\n::: {#b9ae68a8 .cell execution_count=12}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=12, seed=0, n_jobs=3)\nresult = opt.optimize()\nassert result.nfev <= 12\nprint(f\"n_jobs : {opt.n_jobs}\")\nprint(f\"evaluations : {result.nfev}\")\nprint(\"search slot management check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nn_jobs : 3\nevaluations : 12\nsearch slot management check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 13 — Candidate Generation (`suggest_next_infill_point()`)\n\n```python\ndef _thread_search_task():\n with _surrogate_lock:\n return self.suggest_next_infill_point()\n```\n\n`suggest_next_infill_point()` runs the acquisition function to identify the most\npromising candidate for the next objective evaluation.\nThe acquisition strategy is controlled by `acquisition` (default `\"y\"`, minimising the\nsurrogate prediction directly).\nThe acquisition optimiser (default `\"differential_evolution\"`) searches the transformed,\nreduced search space.\nWhen the optimiser fails, the fallback strategy selected by\n`acquisition_failure_strategy` applies; `\"random\"` (the default) draws a uniform random\npoint.\nIn the parallel path this method is always called inside `_thread_search_task()`, which\nholds `_surrogate_lock` for the duration of the call, preventing simultaneous surrogate\naccess by multiple search threads or by the main-thread refit.\n\n::: {#878b1a51 .cell execution_count=13}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, acquisition=\"ei\", seed=0, n_jobs=2)\nresult = opt.optimize()\nassert opt.acquisition == \"ei\"\nprint(f\"acquisition : {opt.acquisition}\")\nprint(f\"best : {result.fun:.6f}\")\nprint(\"suggest_next_infill_point check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nacquisition : ei\nbest : 1.083099\nsuggest_next_infill_point check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 14 — Future Completion and Routing\n\n```python\ndone, _ = wait(futures.keys(), return_when=FIRST_COMPLETED)\nfor fut in done:\n ftype = futures.pop(fut)\n res = fut.result()\n if ftype == \"search\":\n x_cand = res\n pending_cands.append(x_cand)\n if _batch_ready():\n _flush_batch()\n elif ftype == \"batch_eval\":\n _future_n_pts.pop(fut, None)\n X_done, y_done = res\n # process batch...\n```\n\n`concurrent.futures.wait()` with `return_when=FIRST_COMPLETED` blocks until at least\none future finishes, then returns all futures that are done.\nEach completed future is popped from `futures` and routed by its type tag.\nFor a `\"search\"` future the returned candidate is appended to `pending_cands`; if this\npushes the list over the batch threshold, `_flush_batch()` is called immediately to\navoid an unnecessary extra loop iteration.\nFor a `\"batch_eval\"` future the corresponding entry in `_future_n_pts` is removed so\nthat budget accounting is unblocked before the storage update begins.\nIf a result is an `Exception` — indicating a remote failure — the error is optionally\nprinted, the budget entry is removed, and the loop continues; failed search slots are\nrefilled in the next iteration, and failed eval points are lost without charging the\nbudget counter.\n\n::: {#6e6ab5ac .cell execution_count=14}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=15, seed=0, n_jobs=2)\nresult = opt.optimize()\nassert len(opt.y_) <= 15\nassert np.all(np.isfinite(opt.y_))\nprint(f\"evaluations stored : {len(opt.y_)}\")\nprint(\"future routing check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nevaluations stored : 15\nfuture routing check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 15 — Batch Evaluation Processing (`update_success_rate()`, `_update_storage_steady()`, `fit_scheduler()`)\n\n```python\nfor xi, yi in zip(X_done, y_done):\n self.update_success_rate(np.array([yi]))\n self._update_storage_steady(xi, yi)\n self.n_iter_ += 1\nwith _surrogate_lock:\n self.fit_scheduler()\n```\n\nWhen a batch evaluation completes successfully, the main thread processes every point in\nthe returned `(X_done, y_done)` pair before refitting the surrogate.\nFor each point, `update_success_rate()` records whether the new value improves on\n`best_y_`, maintaining the rolling `success_rate` attribute.\n`_update_storage_steady()` appends the point to `X_` and `y_`, updates `best_x_` and\n`best_y_` if an improvement is found, and synchronises `min_y` and `min_X`.\n`n_iter_` is incremented once per point so that it reflects the total number of\npost-initial-design evaluations.\nAfter all points in the batch have been stored, `fit_scheduler()` is called once under\n`_surrogate_lock`, refitting the surrogate on the updated training window.\nBatching the refit in this way — one call per batch rather than one call per point —\nimproves efficiency when `eval_batch_size > 1` and ensures that in-flight search\nthreads always read a self-consistent model.\n\n::: {#58e66c1d .cell execution_count=15}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=15, seed=0, n_jobs=2)\nopt.optimize()\nprint(f\"success_rate : {opt.success_rate:.4f}\")\nprint(f\"X_ shape : {opt.X_.shape}\")\nprint(f\"y_ shape : {opt.y_.shape}\")\nassert 0.0 <= opt.success_rate <= 1.0\nassert opt.X_.shape[0] == opt.y_.shape[0]\nprint(\"batch processing check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nsuccess_rate : 0.6000\nX_ shape : (15, 2)\ny_ shape : (15,)\nbatch processing check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 16 — Termination and Result Assembly\n\n```python\nreturn \"FINISHED\", OptimizeResult(\n x=self.best_x_,\n fun=self.best_y_,\n nfev=len(self.y_),\n nit=self.n_iter_,\n success=True,\n message=\"Optimization finished (Steady State)\",\n X=self.X_,\n y=self.y_,\n)\n```\n\n`optimize_steady_state()` exits the while loop when `len(y_) >= effective_max_iter` or\nthe wall-clock limit is exceeded.\nIt always returns `\"FINISHED\"` — there is no restart mechanism in the parallel path.\nThe `OptimizeResult` object carries the best solution (`x`, `fun`), total evaluation\ncount (`nfev`), iteration count (`nit`), the full evaluation history (`X`, `y`), and a\nfixed termination message that identifies the result as coming from the steady-state\nengine.\nThe `success` flag is always `True` because partial results are still valid whenever at\nleast one evaluation completed successfully; the guard at the end of Phase 1 ensures\nthe method does not return an empty result.\n\n::: {#a58b8008 .cell execution_count=16}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, seed=0, n_jobs=2)\nresult = opt.optimize()\nfirst_line = result.message.splitlines()[0]\nprint(f\"termination : {first_line}\")\nassert \"Steady State\" in result.message\nassert result.success\nprint(\"termination check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ntermination : Optimization finished (Steady State)\ntermination check passed.\n```\n:::\n:::\n\n\n---\n\n## Complete Parallel Run Summary\n\n@tbl-par summarises every step executed along the parallel path in call order:\n\n| Step | Method / Phase | Purpose |\n|-----:|----------------|---------|\n| 1 | `execute_optimization_run()` | Dispatch to parallel path when `n_jobs > 1` |\n| 2 | `optimize_steady_state()` | Parallel orchestrator; manages pools and phases |\n| 3 | `set_seed()`, `get_initial_design()`, `curate_initial_design()` | Seed RNG, generate and curate initial design; pre-fill `y0_prefilled` for restart-injected point |\n| 4 | `is_gil_disabled()` | Detect GIL state; select `ProcessPoolExecutor` or `ThreadPoolExecutor` for eval |\n| 5 | Phase 1 submission | Store injected points directly; submit remaining `n_to_submit` points to `eval_pool` |\n| 6 | `_update_storage_steady()` | Collect initial results; append each valid `(x, y)` to storage |\n| 7 | `_init_tensorboard()`, `update_stats()`, `get_best_xy_initial_design()` | Log initial design; compute statistics; identify initial best |\n| 8 | `fit_scheduler()` | First surrogate fit (no lock needed, no search threads active) |\n| 9 | `optimize_steady_state()` while loop | Main loop: iterate until budget or time exhausted |\n| 10 | `_batch_ready()` | Check whether `pending_cands` should be flushed |\n| 11 | `_flush_batch()` | Dispatch all pending candidates as one batch eval to `eval_pool` |\n| 12 | `_thread_search_task()` slot fill | Submit up to `n_jobs` search tasks under budget guard |\n| 13 | `suggest_next_infill_point()` | Optimise acquisition under `_surrogate_lock` to propose candidate |\n| 14 | `wait(FIRST_COMPLETED)` routing | Block until any future completes; route by `\"search\"` or `\"batch_eval\"` tag |\n| 15 | `update_success_rate()`, `_update_storage_steady()`, `fit_scheduler()` | Process batch result; update storage and best; refit surrogate under lock |\n| 16 | Return `\"FINISHED\"` + `OptimizeResult` | Assemble and return final result |\n\n: Complete Parallel Run Summary {#tbl-par}\n\n", - "supporting": [ - "optimize_parallel_files/figure-html" - ], - "filters": [], - "includes": {} - } -} \ No newline at end of file diff --git a/_freeze/docs/optimize_seq/execute-results/html.json b/_freeze/docs/optimize_seq/execute-results/html.json index 0038f327..39d5dd23 100644 --- a/_freeze/docs/optimize_seq/execute-results/html.json +++ b/_freeze/docs/optimize_seq/execute-results/html.json @@ -1,8 +1,8 @@ { - "hash": "197bb2215211b4ea7119f67337174429", + "hash": "b0268e3e8a4b9f253a73fdc639078314", "result": { "engine": "jupyter", - "markdown": "---\ntitle: \"SpotOptim: Sequential Optimization\"\ndescription: \"Step-by-step walkthrough of execute_optimization_run() and every method it calls along the sequential path, with executable examples validated by pytest.\"\n---\n\nThis document traces every step executed by `SpotOptim.execute_optimization_run()` along\nthe sequential code path (`n_jobs=1`), in the order they occur.\nEach section describes one method with a `{python}` code block that can be executed directly.\n\nThe public entry point is `optimize()`, which manages the outer restart loop and delegates\neach cycle to `execute_optimization_run()`.\nWhen `n_jobs == 1` (the default), that dispatcher routes to `optimize_sequential_run()`,\nwhich coordinates initialisation, storage setup, and the main iteration loop.\nThis document covers that path in full.\n\nRun all related tests with:\n\n```bash\nuv run pytest tests/test_spotoptim_deep.py -v\n```\n\n---\n\n## Step 1 — Dispatch (`execute_optimization_run()`)\n\n```python\nif self.n_jobs > 1:\n return self.optimize_steady_state(...)\nelse:\n return self.optimize_sequential_run(...)\n```\n\n`execute_optimization_run()` is the routing layer between the outer restart loop in\n`optimize()` and the actual optimisation engine.\nIts sole responsibility is to examine `n_jobs` and forward all arguments to either\n`optimize_steady_state()` (parallel) or `optimize_sequential_run()` (sequential).\nIt returns a `(status, OptimizeResult)` tuple in both cases, which `optimize()` uses to\ndecide whether to restart or terminate.\nThe optional `shared_best_y` and `shared_lock` parameters support inter-worker\ncoordination in the parallel path; they are `None` in sequential mode.\n\n::: {#5d8986fb .cell execution_count=1}\n``` {.python .cell-code}\nimport time\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, seed=0, n_jobs=1)\nstatus, result = opt.execute_optimization_run(timeout_start=time.time())\nprint(f\"status : {status}\")\nprint(f\"best : {result.fun:.6f}\")\nassert status == \"FINISHED\"\nprint(\"dispatch check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nstatus : FINISHED\nbest : 0.022050\ndispatch check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 2 — Sequential Run Orchestration (`optimize_sequential_run()`)\n\n```python\nX0, y0 = self._initialize_run(X0, y0_known)\nX0, y0, n_evaluated = self.rm_initial_design_NA_values(X0, y0)\nself.check_size_initial_design(y0, n_evaluated)\nself.init_storage(X0, y0)\nself._zero_success_count = 0\nself._success_history = []\nself.update_stats()\nself._init_tensorboard()\nself.get_best_xy_initial_design()\nreturn self._run_sequential_loop(timeout_start, effective_max_iter)\n```\n\n`optimize_sequential_run()` is the sequential orchestrator.\nIt calls eight methods in fixed order to prepare internal state before handing off\nto `_run_sequential_loop()` for the iterative acquisition phase.\nThe zero-success counter and success-history list are reset here so that each fresh\nrun (including restarts) begins with a clean convergence record.\nWhen `max_iter_override` is supplied by `optimize()`, it replaces the configured\n`max_iter` as the effective evaluation budget for this run.\n\n::: {#43809ded .cell execution_count=2}\n``` {.python .cell-code}\nimport time\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, seed=0)\nstatus, result = opt.optimize_sequential_run(timeout_start=time.time())\nprint(f\"status : {status}\")\nprint(f\"evaluations : {result.nfev}\")\nprint(f\"best : {result.fun:.6f}\")\nassert status == \"FINISHED\"\nassert result.nfev == 10\nprint(\"sequential run check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nstatus : FINISHED\nevaluations : 10\nbest : 0.022050\nsequential run check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 3 — Initialisation (`_initialize_run()`)\n\n```python\nself.set_seed()\nX0 = self.get_initial_design(X0)\nX0 = self.curate_initial_design(X0)\ny0 = self.evaluate_function(X0)\nreturn X0, y0\n```\n\n`_initialize_run()` performs three preparatory actions before the first surrogate can\nbe fitted.\n`set_seed()` re-seeds Python's `random` module and NumPy's global generator to ensure\nreproducibility within the run.\n`get_initial_design()` either processes the user-supplied `X0` or generates a\nLatin Hypercube sample in the transformed, reduced search space.\n`curate_initial_design()` removes duplicate points and generates replacements as needed.\nThe curated design is then evaluated in batch via `evaluate_function()`.\n\nWhen a best-known value `y0_known` is provided (restart injection), the point that\nmatches `self.x0` is not re-evaluated; its objective value is taken directly from the\nprevious run, saving one function call.\n\n::: {#f3fdd046 .cell execution_count=3}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], n_initial=5, seed=42)\nX0, y0 = opt._initialize_run(X0=None, y0_known=None)\nprint(f\"initial design shape : {X0.shape}\")\nprint(f\"evaluations : {len(y0)}\")\nassert X0.shape == (5, 2)\nassert len(y0) == 5\nassert np.all(np.isfinite(y0))\nprint(\"_initialize_run check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ninitial design shape : (5, 2)\nevaluations : 5\n_initialize_run check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 4 — Filtering Invalid Evaluations (`rm_initial_design_NA_values()`)\n\n```python\nfinite_mask = np.isfinite(y0)\nX0 = X0[finite_mask]\ny0 = y0[finite_mask]\nreturn X0, y0, len(finite_mask)\n```\n\nInitial design points whose objective value is `NaN` or `±inf` are removed rather than\npenalised.\nThis is the correct policy for the initial phase: a penalty value would corrupt the\nsurrogate's training data, whereas removal simply reduces the effective initial design\nsize.\nThe method also converts object-dtype arrays (which may contain Python `None`) to\n`float` before applying the mask.\nThe original count is returned as the third value so that `check_size_initial_design()`\ncan report how many points were lost.\n\n::: {#0f34d1ed .cell execution_count=4}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], n_initial=5)\nX0 = np.array([[1.0, 2.0], [3.0, 4.0], [0.0, 0.0]])\ny0 = np.array([5.0, np.nan, 0.0])\nX0_clean, y0_clean, n_original = opt.rm_initial_design_NA_values(X0, y0)\nassert X0_clean.shape == (2, 2)\nassert len(y0_clean) == 2\nassert n_original == 3\nassert np.all(np.isfinite(y0_clean))\nprint(f\"1 NaN removed; {len(y0_clean)} of {n_original} points retained.\")\nprint(\"rm_initial_design_NA_values check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n1 NaN removed; 2 of 3 points retained.\nrm_initial_design_NA_values check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 5 — Size Validation (`check_size_initial_design()`)\n\n```python\nmin_required = min(n_initial, 3 if n_dim > 1 else 2)\nif len(y0) < min_required:\n raise ValueError(...)\n```\n\nBefore fitting the first surrogate, the optimizer verifies that enough valid initial\npoints remain.\nThe minimum accepted count is the smaller of `n_initial` and the surrogate's structural\nminimum — 3 for multi-dimensional problems, 2 for one-dimensional ones.\nIf the filtered design falls below this threshold, a `ValueError` is raised with a\ndiagnostic message.\nThe threshold adapts to the user's intent: when `n_initial` was set to 2, only 2 points\nare required; the structural minimum only applies when more points were requested than\nsurvived filtering.\n\n::: {#69bac1e2 .cell execution_count=5}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], n_initial=10)\n\ny0_ok = np.array([1.0, 2.0, 3.0, 4.0])\nopt.check_size_initial_design(y0_ok, n_evaluated=10)\nprint(\"Sufficient points: OK\")\n\ny0_tiny = np.array([1.0])\ntry:\n opt.check_size_initial_design(y0_tiny, n_evaluated=10)\n raise AssertionError(\"Expected ValueError not raised\")\nexcept ValueError as e:\n print(f\"Caught expected error: {e}\")\nprint(\"check_size_initial_design check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSufficient points: OK\nCaught expected error: Insufficient valid initial design points: only 1 finite value(s) out of 10 evaluated. Need at least 3 points to fit surrogate model. Please check your objective function or increase n_initial.\ncheck_size_initial_design check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 6 — Storage Initialisation (`init_storage()`)\n\n```python\nself.X_ = self.inverse_transform_X(X0.copy())\nself.y_ = y0.copy()\nself.n_iter_ = 0\n```\n\n`init_storage()` populates the two primary data arrays that persist throughout the run.\n`X_` is stored in natural (original) scale by applying `inverse_transform_X()` to the\ninternally scaled design; `y_` is stored as-is.\nThe iteration counter `n_iter_` is reset to zero.\nAll subsequent storage operations in the main loop append to these arrays rather than\nreplacing them, so the complete evaluation history is available at the end of the run.\n\n::: {#08796c69 .cell execution_count=6}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)], n_initial=3)\nX0 = np.array([[1.0, 2.0], [0.0, 0.0], [3.0, -1.0]])\ny0 = np.array([5.0, 0.0, 10.0])\nopt.init_storage(X0, y0)\nprint(f\"X_ shape : {opt.X_.shape}\")\nprint(f\"y_ : {opt.y_}\")\nprint(f\"n_iter_ : {opt.n_iter_}\")\nassert opt.X_.shape == (3, 2)\nassert opt.n_iter_ == 0\nprint(\"init_storage check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nX_ shape : (3, 2)\ny_ : [ 5. 0. 10.]\nn_iter_ : 0\ninit_storage check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 7 — Statistics Update (`update_stats()`)\n\n```python\nself.min_y = np.min(self.y_)\nself.min_X = self.X_[np.argmin(self.y_)]\nself.counter = len(self.y_)\n```\n\n`update_stats()` refreshes the summary statistics derived from the current `X_` and\n`y_` arrays.\nIt always sets `min_y`, `min_X`, and `counter`.\nWhen the problem is noisy (`repeats_initial > 1` or `repeats_surrogate > 1`), it\nadditionally computes per-point means and variances via `aggregate_mean_var()`,\npopulating `mean_X`, `mean_y`, `var_y`, `min_mean_X`, `min_mean_y`, and `min_var_y`.\nThe method is called during setup — after `init_storage()` — and once per iteration\ninside the main loop after new points have been appended.\n\n::: {#160ff8fa .cell execution_count=7}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n max_iter=10, n_initial=5, seed=0)\nopt.optimize()\nprint(f\"counter : {opt.counter}\")\nprint(f\"min_y : {opt.min_y:.6f}\")\nassert opt.counter == 10\nassert np.isclose(opt.min_y, np.min(opt.y_))\nprint(\"update_stats check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ncounter : 10\nmin_y : 0.022050\nupdate_stats check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 8 — TensorBoard Logging of the Initial Design (`_init_tensorboard()`)\n\n```python\nif self.tb_writer is not None:\n for i in range(len(self.y_)):\n self._write_tensorboard_hparams(self.X_[i], self.y_[i])\n self._write_tensorboard_scalars()\n```\n\n`_init_tensorboard()` logs each point of the initial design to TensorBoard as a\nseparate hyperparameter run, together with global scalar summaries.\nWhen `tensorboard_log=False` (the default), the writer is `None` and the method is a\nno-op with no runtime cost.\nWhen logging is enabled and no writer exists yet, `_init_tensorboard()` creates the\n`SummaryWriter`, choosing a timestamped directory if `tensorboard_path` was not\nspecified.\nThis lazy creation avoids producing stale log directories for runs that fail during\ninitialisation.\n\n::: {#f322e629 .cell execution_count=8}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)], tensorboard_log=False)\nopt.optimize()\nassert opt.tb_writer is None\nprint(\"TensorBoard disabled: writer not created.\")\nprint(\"_init_tensorboard check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard disabled: writer not created.\n_init_tensorboard check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 9 — Initial Best (`get_best_xy_initial_design()`)\n\n```python\nbest_idx = np.argmin(self.y_)\nself.best_x_ = self.X_[best_idx].copy()\nself.best_y_ = self.y_[best_idx]\n```\n\nAfter the initial design is stored and its statistics computed, the point with the\nminimum objective value is identified and written to `best_x_` and `best_y_`.\nThese two attributes define the running best solution, updated by\n`_update_best_main_loop()` in every subsequent iteration.\nWhen `verbose=True`, the initial best is printed; for noisy problems the mean best\n(`min_mean_y`) is also reported alongside the raw minimum.\n\n::: {#88bee30d .cell execution_count=9}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], n_initial=5, verbose=False)\nopt.X_ = np.array([[1.0, 2.0], [0.0, 0.0], [2.0, 1.0]])\nopt.y_ = np.array([5.0, 0.0, 5.0])\nopt.get_best_xy_initial_design()\nassert np.array_equal(opt.best_x_, [0.0, 0.0])\nassert opt.best_y_ == 0.0\nprint(f\"best_x_ : {opt.best_x_}\")\nprint(f\"best_y_ : {opt.best_y_}\")\nprint(\"get_best_xy_initial_design check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nbest_x_ : [0. 0.]\nbest_y_ : 0.0\nget_best_xy_initial_design check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 10 — Main Iteration Loop (`_run_sequential_loop()`)\n\n```python\nwhile len(self.y_) < effective_max_iter and \\\n time.time() < timeout_start + max_time * 60:\n self.n_iter_ += 1\n self.fit_scheduler()\n X_ocba = self.apply_ocba()\n x_next = self.suggest_next_infill_point()\n x_next_repeated = self.update_repeats_infill_points(x_next)\n if X_ocba is not None:\n x_next_repeated = append(X_ocba, x_next_repeated, axis=0)\n y_next = self.evaluate_function(x_next_repeated)\n x_next_repeated, y_next = self._handle_NA_new_points(x_next_repeated, y_next)\n self.update_success_rate(y_next)\n # restart check\n self.update_storage(x_next_repeated, y_next)\n self.update_stats()\n self._update_best_main_loop(x_next_repeated, y_next, start_time=timeout_start)\n```\n\n`_run_sequential_loop()` executes iterations until either the evaluation budget\n(`effective_max_iter`) or the wall-clock limit (`max_time` minutes) is exhausted.\nEach iteration increments `n_iter_`, fits the surrogate, selects and evaluates one or\nmore candidate points, and updates internal state.\nA safety counter tracks consecutive failures: more than `max_iter` consecutive\nNaN/inf evaluations triggers an early exit with `success=False`.\n\nThe loop returns `(\"RESTART\", result)` when `success_rate` has been zero for\n`restart_after_n` consecutive iterations, signalling `optimize()` to begin a fresh run.\nIt returns `(\"FINISHED\", result)` when the budget or time limit is reached.\n\n::: {#2e901cb9 .cell execution_count=10}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=15, seed=0)\nresult = opt.optimize()\nprint(f\"iterations : {result.nit}\")\nprint(f\"evaluations : {result.nfev}\")\nprint(f\"best : {result.fun:.6f}\")\nassert result.nfev == 15\nassert result.success\nprint(\"_run_sequential_loop check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\niterations : 10\nevaluations : 15\nbest : 0.000001\n_run_sequential_loop check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 11 — Surrogate Fitting (`fit_scheduler()`)\n\n```python\nself.fit_scheduler()\n```\n\nAt the start of each iteration, the surrogate model is refitted to the current training\nwindow.\n`fit_scheduler()` selects the most recent `window_size` observations according to\n`selection_method` (default `\"distant\"`) and calls the surrogate's `fit()` method.\nWhen a list of surrogates was supplied at construction, one surrogate is chosen\nprobabilistically according to `prob_surrogate` before fitting, and per-surrogate\npoint caps from `_max_surrogate_points_list` are respected.\n\n::: {#8d2b4bf3 .cell execution_count=11}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=15, window_size=10, seed=0)\nresult = opt.optimize()\nprint(f\"window_size : {opt.window_size}\")\nprint(f\"evaluations : {result.nfev}\")\nassert opt.window_size == 10\nprint(\"fit_scheduler check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nwindow_size : 10\nevaluations : 15\nfit_scheduler check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 12 — OCBA Re-evaluations (`apply_ocba()`)\n\n```python\nX_ocba = self.apply_ocba()\n```\n\n`apply_ocba()` implements Optimal Computing Budget Allocation for noisy objective\nfunctions.\nWhen `ocba_delta > 0`, it identifies the `ocba_delta` best-mean points and schedules\nadditional evaluations at those locations, returning them as `X_ocba`.\nThese points are concatenated with the acquisition candidate before the objective call,\nso that noisy regions near the current optimum receive extra replication.\nWhen `ocba_delta == 0` (the default), the method returns `None` and adds no overhead.\n\n::: {#beee997f .cell execution_count=12}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, ocba_delta=0, seed=0)\nresult = opt.optimize()\nassert opt.ocba_delta == 0\nprint(f\"ocba_delta : {opt.ocba_delta} (no OCBA overhead)\")\nprint(\"apply_ocba check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nocba_delta : 0 (no OCBA overhead)\napply_ocba check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 13 — Candidate Generation (`suggest_next_infill_point()`)\n\n```python\nx_next = self.suggest_next_infill_point()\n```\n\n`suggest_next_infill_point()` runs the acquisition function to identify the most\npromising candidate for the next objective evaluation.\nThe acquisition strategy is controlled by `acquisition` (default `\"y\"`, minimising the\nsurrogate prediction directly).\nThe acquisition optimiser (default `\"differential_evolution\"`) searches the\ntransformed, reduced search space using the bounds `[lower, upper]`.\nWhen the optimiser fails — no improvement found or a numerical issue — the strategy\nselected by `acquisition_failure_strategy` takes effect; `\"random\"` (the default)\ndraws a point uniformly at random.\n\n::: {#5b42e947 .cell execution_count=13}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, acquisition=\"ei\", seed=0)\nresult = opt.optimize()\nassert opt.acquisition == \"ei\"\nprint(f\"acquisition : {opt.acquisition}\")\nprint(f\"best : {result.fun:.6f}\")\nprint(\"suggest_next_infill_point check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nacquisition : ei\nbest : 0.304090\nsuggest_next_infill_point check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 14 — Repeat Infill Points (`update_repeats_infill_points()`)\n\n```python\nx_next_repeated = self.update_repeats_infill_points(x_next)\n```\n\nWhen `repeats_surrogate > 1`, each surrogate-suggested candidate is evaluated multiple\ntimes to reduce noise.\n`update_repeats_infill_points()` tiles the candidate point `repeats_surrogate` times,\nreturning a 2-D array of shape `(repeats_surrogate, n_dim)`.\nWhen `repeats_surrogate == 1` (the default) the array is simply `x_next` reshaped to\n`(1, n_dim)`, adding no computational cost.\n\n::: {#d6e35dbc .cell execution_count=14}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, repeats_surrogate=1, seed=0)\nresult = opt.optimize()\nassert opt.repeats_surrogate == 1\nassert result.nfev == 10\nprint(f\"repeats_surrogate : {opt.repeats_surrogate}\")\nprint(\"update_repeats_infill_points check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nrepeats_surrogate : 1\nupdate_repeats_infill_points check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 15 — Objective Evaluation (`evaluate_function()`)\n\n```python\ny_next = self.evaluate_function(x_next_repeated)\n```\n\n`evaluate_function()` calls the user-supplied objective `fun` on the batch of\ncandidate points.\nThe input is always in transformed, reduced internal scale;\n`evaluate_function()` applies `inverse_transform_X()` and `to_all_dim()` before the\ncall, so `fun` always receives points in natural scale with all dimensions present.\nWhen `fun_mo2so` is set, the multi-objective output is first converted to a scalar\nusing that aggregation function.\nThe result is a 1-D array whose length equals the number of candidates evaluated.\n\n::: {#064fb2d4 .cell execution_count=15}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, seed=0)\nresult = opt.optimize()\nassert len(opt.y_) == 10\nassert np.all(np.isfinite(opt.y_))\nprint(f\"total evaluations : {len(opt.y_)}\")\nprint(f\"best value : {opt.min_y:.6f}\")\nprint(\"evaluate_function check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ntotal evaluations : 10\nbest value : 0.022050\nevaluate_function check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 16 — NaN Handling for Sequential Evaluations (`_handle_NA_new_points()`)\n\n```python\nx_next_repeated, y_next = self._handle_NA_new_points(x_next_repeated, y_next)\n```\n\nUnlike the initial design (where invalid points are removed), NaN or inf values\nreturned during the sequential loop are replaced with a penalty derived from the worst\nfinite value seen so far, scaled by a large factor.\nThis preserves the storage structure — one row in `X_` per candidate — and prevents\nthe surrogate from ignoring pathological regions.\nIf every candidate in the batch is invalid, the method returns `(None, None)`, causing\nthe iteration to be skipped and the consecutive-failure counter to increment.\n\n::: {#d91c0178 .cell execution_count=16}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=12, seed=0)\nopt.optimize()\nassert np.all(np.isfinite(opt.y_))\nprint(\"All stored values are finite after NaN handling.\")\nprint(\"_handle_NA_new_points check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nAll stored values are finite after NaN handling.\n_handle_NA_new_points check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 17 — Success Rate (`update_success_rate()`)\n\n```python\nself.update_success_rate(y_next)\n```\n\n`update_success_rate()` measures whether the current iteration produced an improvement\nrelative to `best_y_`.\nIt records a binary outcome in `_success_history` and computes `success_rate` as the\nfraction of recent iterations that showed improvement.\nWhen `success_rate` remains at `0.0` for `restart_after_n` consecutive iterations,\n`_zero_success_count` reaches the threshold and the loop returns `\"RESTART\"` to the\ncaller.\n\n::: {#4c502382 .cell execution_count=17}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=15, seed=0)\nopt.optimize()\nprint(f\"success_rate : {opt.success_rate:.4f}\")\nassert 0.0 <= opt.success_rate <= 1.0\nprint(\"update_success_rate check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nsuccess_rate : 0.8000\nupdate_success_rate check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 18 — Storage Update (`update_storage()`)\n\n```python\nself.update_storage(x_next_repeated, y_next)\n```\n\n`update_storage()` appends newly evaluated candidates to `X_` and `y_`.\nLike `init_storage()`, it converts points to natural scale via `inverse_transform_X()`\nbefore storing.\nAfter each call `X_.shape[0]` increases by the number of candidates evaluated — usually\n1, but more when `repeats_surrogate > 1` or OCBA is active.\nThe growing `X_` and `y_` arrays serve both as the surrogate training window and as\nthe final output embedded in `OptimizeResult`.\n\n::: {#6e6fb808 .cell execution_count=18}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, seed=0)\nopt.optimize()\nassert opt.X_.shape == (10, 2)\nassert opt.y_.shape == (10,)\nprint(f\"X_ shape : {opt.X_.shape}\")\nprint(f\"y_ shape : {opt.y_.shape}\")\nprint(\"update_storage check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nX_ shape : (10, 2)\ny_ shape : (10,)\nupdate_storage check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 19 — Best Solution Update (`_update_best_main_loop()`)\n\n```python\nself._update_best_main_loop(x_next_repeated, y_next, start_time=timeout_start)\n```\n\nAt the end of each iteration, `_update_best_main_loop()` checks whether any of the\nnewly evaluated points improves on `best_y_`.\nIf so, `best_x_` and `best_y_` are updated in-place.\nWhen `verbose=True`, the improvement is printed together with elapsed time and the\ncurrent evaluation count.\nFor noisy problems, improvement is judged against `min_mean_y` rather than the raw\nminimum.\n\n::: {#61746f16 .cell execution_count=19}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=15, seed=0)\nresult = opt.optimize()\nassert np.isclose(opt.best_y_, result.fun)\nassert np.array_equal(opt.best_x_, result.x)\nprint(f\"best_x_ : {opt.best_x_}\")\nprint(f\"best_y_ : {opt.best_y_:.6f}\")\nprint(\"_update_best_main_loop check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nbest_x_ : [7.26480048e-04 3.90431069e-05]\nbest_y_ : 0.000001\n_update_best_main_loop check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 20 — Termination (`determine_termination()`)\n\n```python\nstatus_message = self.determine_termination(timeout_start)\n```\n\nAfter the while condition fails, `determine_termination()` produces the human-readable\ntermination message embedded in `OptimizeResult.message`.\nIt distinguishes three cases: the evaluation budget was exhausted\n(`nfev >= effective_max_iter`), the wall-clock limit was exceeded (`max_time`), or the\ntolerance criterion was met — consecutive best-point improvements smaller than\n`tolerance_x` measured by `min_tol_metric`.\nThe formatted message also includes the final function value, iteration count, and\ntotal evaluation count, matching the style of `scipy.optimize.minimize`.\n\n::: {#a7f44173 .cell execution_count=20}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, seed=0)\nresult = opt.optimize()\nfirst_line = result.message.splitlines()[0]\nprint(f\"termination : {first_line}\")\nassert \"10\" in result.message\nprint(\"determine_termination check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ntermination : Optimization terminated: maximum evaluations (10) reached\ndetermine_termination check passed.\n```\n:::\n:::\n\n\n---\n\n## Complete Sequential Run Summary\n\n@tbl-seq summarises every step executed along the sequential path in call order:\n\n| Step | Method | Purpose |\n|-----:|--------|---------|\n| 1 | `execute_optimization_run()` | Dispatch to sequential or parallel path |\n| 2 | `optimize_sequential_run()` | Sequential orchestrator |\n| 3 | `_initialize_run()` | Seed RNG, generate and evaluate initial design |\n| 4 | `rm_initial_design_NA_values()` | Remove NaN/inf from initial evaluations |\n| 5 | `check_size_initial_design()` | Validate minimum initial design size |\n| 6 | `init_storage()` | Initialise `X_`, `y_`, `n_iter_` |\n| 7 | `update_stats()` | Compute `min_y`, `min_X`, `counter` |\n| 8 | `_init_tensorboard()` | Log initial design to TensorBoard |\n| 9 | `get_best_xy_initial_design()` | Identify initial `best_x_`, `best_y_` |\n| 10 | `_run_sequential_loop()` | Main iteration loop (Steps 11–20 per iteration) |\n| 11 | `fit_scheduler()` | Fit surrogate to current training window |\n| 12 | `apply_ocba()` | Schedule OCBA re-evaluations (noisy problems only) |\n| 13 | `suggest_next_infill_point()` | Optimise acquisition to propose candidate |\n| 14 | `update_repeats_infill_points()` | Replicate candidate for noisy evaluation |\n| 15 | `evaluate_function()` | Call `fun` in natural scale |\n| 16 | `_handle_NA_new_points()` | Penalise or skip invalid evaluations |\n| 17 | `update_success_rate()` | Track improvement rate; trigger restart if stalled |\n| 18 | `update_storage()` | Append candidates to `X_`, `y_` |\n| 19 | `update_stats()` | Refresh statistics after new evaluations |\n| 20 | `_update_best_main_loop()` | Update `best_x_`, `best_y_` |\n| 21 | `determine_termination()` | Produce termination message and `OptimizeResult` |\n\n: Complete Sequential Run Summary {#tbl-seq}\n\n", + "markdown": "---\ntitle: \"SpotOptim: Sequential Optimization\"\ndescription: \"Step-by-step walkthrough of execute_optimization_run() and every method it calls along the sequential path, with executable examples validated by pytest.\"\n---\n\nThis document traces every step executed by `SpotOptim.execute_optimization_run()` along\nthe sequential optimization path, in the order they occur.\nEach section describes one method with a `{python}` code block that can be executed directly.\n\nThe public entry point is `optimize()`, which manages the outer restart loop and delegates\neach cycle to `execute_optimization_run()`.\nThat method routes to `optimize_sequential_run()`, which coordinates initialisation,\nstorage setup, and the main iteration loop.\nThis document covers that path in full.\n\nRun all related tests with:\n\n```bash\nuv run pytest tests/test_spotoptim_deep.py -v\n```\n\n---\n\n## Step 1 — Dispatch (`execute_optimization_run()`)\n\n`execute_optimization_run()` is the thin pass-through between the outer restart loop in\n`optimize()` and the sequential optimisation engine.\nIt forwards all arguments to `optimize_sequential_run()` and returns a\n`(status, OptimizeResult)` tuple, which `optimize()` uses to decide whether to restart\nor terminate.\n\n::: {#78693c4a .cell execution_count=1}\n``` {.python .cell-code}\nimport time\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, seed=0)\nstatus, result = opt.execute_optimization_run(timeout_start=time.time())\nprint(f\"status : {status}\")\nprint(f\"best : {result.fun:.6f}\")\nassert status == \"FINISHED\"\nprint(\"dispatch check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nstatus : FINISHED\nbest : 0.022050\ndispatch check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 2 — Sequential Run Orchestration (`optimize_sequential_run()`)\n\n```python\nX0, y0 = self._initialize_run(X0, y0_known)\nX0, y0, n_evaluated = self.rm_initial_design_NA_values(X0, y0)\nself.check_size_initial_design(y0, n_evaluated)\nself.init_storage(X0, y0)\nself._zero_success_count = 0\nself._success_history = []\nself.update_stats()\nself._init_tensorboard()\nself.get_best_xy_initial_design()\nreturn self._run_sequential_loop(timeout_start, effective_max_iter)\n```\n\n`optimize_sequential_run()` is the sequential orchestrator.\nIt calls eight methods in fixed order to prepare internal state before handing off\nto `_run_sequential_loop()` for the iterative acquisition phase.\nThe zero-success counter and success-history list are reset here so that each fresh\nrun (including restarts) begins with a clean convergence record.\nWhen `max_iter_override` is supplied by `optimize()`, it replaces the configured\n`max_iter` as the effective evaluation budget for this run.\n\n::: {#ac23c7fb .cell execution_count=2}\n``` {.python .cell-code}\nimport time\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, seed=0)\nstatus, result = opt.optimize_sequential_run(timeout_start=time.time())\nprint(f\"status : {status}\")\nprint(f\"evaluations : {result.nfev}\")\nprint(f\"best : {result.fun:.6f}\")\nassert status == \"FINISHED\"\nassert result.nfev == 10\nprint(\"sequential run check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nstatus : FINISHED\nevaluations : 10\nbest : 0.022050\nsequential run check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 3 — Initialisation (`_initialize_run()`)\n\n```python\nself.set_seed()\nX0 = self.get_initial_design(X0)\nX0 = self.curate_initial_design(X0)\ny0 = self.evaluate_function(X0)\nreturn X0, y0\n```\n\n`_initialize_run()` performs three preparatory actions before the first surrogate can\nbe fitted.\n`set_seed()` re-seeds Python's `random` module and NumPy's global generator to ensure\nreproducibility within the run.\n`get_initial_design()` either processes the user-supplied `X0` or generates a\nLatin Hypercube sample in the transformed, reduced search space.\n`curate_initial_design()` removes duplicate points and generates replacements as needed.\nThe curated design is then evaluated in batch via `evaluate_function()`.\n\nWhen a best-known value `y0_known` is provided (restart injection), the point that\nmatches `self.x0` is not re-evaluated; its objective value is taken directly from the\nprevious run, saving one function call.\n\n::: {#736ec647 .cell execution_count=3}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], n_initial=5, seed=42)\nX0, y0 = opt._initialize_run(X0=None, y0_known=None)\nprint(f\"initial design shape : {X0.shape}\")\nprint(f\"evaluations : {len(y0)}\")\nassert X0.shape == (5, 2)\nassert len(y0) == 5\nassert np.all(np.isfinite(y0))\nprint(\"_initialize_run check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ninitial design shape : (5, 2)\nevaluations : 5\n_initialize_run check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 4 — Filtering Invalid Evaluations (`rm_initial_design_NA_values()`)\n\n```python\nfinite_mask = np.isfinite(y0)\nX0 = X0[finite_mask]\ny0 = y0[finite_mask]\nreturn X0, y0, len(finite_mask)\n```\n\nInitial design points whose objective value is `NaN` or `±inf` are removed rather than\npenalised.\nThis is the correct policy for the initial phase: a penalty value would corrupt the\nsurrogate's training data, whereas removal simply reduces the effective initial design\nsize.\nThe method also converts object-dtype arrays (which may contain Python `None`) to\n`float` before applying the mask.\nThe original count is returned as the third value so that `check_size_initial_design()`\ncan report how many points were lost.\n\n::: {#d7abe460 .cell execution_count=4}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], n_initial=5)\nX0 = np.array([[1.0, 2.0], [3.0, 4.0], [0.0, 0.0]])\ny0 = np.array([5.0, np.nan, 0.0])\nX0_clean, y0_clean, n_original = opt.rm_initial_design_NA_values(X0, y0)\nassert X0_clean.shape == (2, 2)\nassert len(y0_clean) == 2\nassert n_original == 3\nassert np.all(np.isfinite(y0_clean))\nprint(f\"1 NaN removed; {len(y0_clean)} of {n_original} points retained.\")\nprint(\"rm_initial_design_NA_values check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n1 NaN removed; 2 of 3 points retained.\nrm_initial_design_NA_values check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 5 — Size Validation (`check_size_initial_design()`)\n\n```python\nmin_required = min(n_initial, 3 if n_dim > 1 else 2)\nif len(y0) < min_required:\n raise ValueError(...)\n```\n\nBefore fitting the first surrogate, the optimizer verifies that enough valid initial\npoints remain.\nThe minimum accepted count is the smaller of `n_initial` and the surrogate's structural\nminimum — 3 for multi-dimensional problems, 2 for one-dimensional ones.\nIf the filtered design falls below this threshold, a `ValueError` is raised with a\ndiagnostic message.\nThe threshold adapts to the user's intent: when `n_initial` was set to 2, only 2 points\nare required; the structural minimum only applies when more points were requested than\nsurvived filtering.\n\n::: {#6b1bcf86 .cell execution_count=5}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], n_initial=10)\n\ny0_ok = np.array([1.0, 2.0, 3.0, 4.0])\nopt.check_size_initial_design(y0_ok, n_evaluated=10)\nprint(\"Sufficient points: OK\")\n\ny0_tiny = np.array([1.0])\ntry:\n opt.check_size_initial_design(y0_tiny, n_evaluated=10)\n raise AssertionError(\"Expected ValueError not raised\")\nexcept ValueError as e:\n print(f\"Caught expected error: {e}\")\nprint(\"check_size_initial_design check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSufficient points: OK\nCaught expected error: Insufficient valid initial design points: only 1 finite value(s) out of 10 evaluated. Need at least 3 points to fit surrogate model. Please check your objective function or increase n_initial.\ncheck_size_initial_design check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 6 — Storage Initialisation (`init_storage()`)\n\n```python\nself.X_ = self.inverse_transform_X(X0.copy())\nself.y_ = y0.copy()\nself.n_iter_ = 0\n```\n\n`init_storage()` populates the two primary data arrays that persist throughout the run.\n`X_` is stored in natural (original) scale by applying `inverse_transform_X()` to the\ninternally scaled design; `y_` is stored as-is.\nThe iteration counter `n_iter_` is reset to zero.\nAll subsequent storage operations in the main loop append to these arrays rather than\nreplacing them, so the complete evaluation history is available at the end of the run.\n\n::: {#05c662f7 .cell execution_count=6}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)], n_initial=3)\nX0 = np.array([[1.0, 2.0], [0.0, 0.0], [3.0, -1.0]])\ny0 = np.array([5.0, 0.0, 10.0])\nopt.init_storage(X0, y0)\nprint(f\"X_ shape : {opt.X_.shape}\")\nprint(f\"y_ : {opt.y_}\")\nprint(f\"n_iter_ : {opt.n_iter_}\")\nassert opt.X_.shape == (3, 2)\nassert opt.n_iter_ == 0\nprint(\"init_storage check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nX_ shape : (3, 2)\ny_ : [ 5. 0. 10.]\nn_iter_ : 0\ninit_storage check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 7 — Statistics Update (`update_stats()`)\n\n```python\nself.min_y = np.min(self.y_)\nself.min_X = self.X_[np.argmin(self.y_)]\nself.counter = len(self.y_)\n```\n\n`update_stats()` refreshes the summary statistics derived from the current `X_` and\n`y_` arrays.\nIt always sets `min_y`, `min_X`, and `counter`.\nWhen the problem is noisy (`repeats_initial > 1` or `repeats_surrogate > 1`), it\nadditionally computes per-point means and variances via `aggregate_mean_var()`,\npopulating `mean_X`, `mean_y`, `var_y`, `min_mean_X`, `min_mean_y`, and `min_var_y`.\nThe method is called during setup — after `init_storage()` — and once per iteration\ninside the main loop after new points have been appended.\n\n::: {#929f32e9 .cell execution_count=7}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n max_iter=10, n_initial=5, seed=0)\nopt.optimize()\nprint(f\"counter : {opt.counter}\")\nprint(f\"min_y : {opt.min_y:.6f}\")\nassert opt.counter == 10\nassert np.isclose(opt.min_y, np.min(opt.y_))\nprint(\"update_stats check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ncounter : 10\nmin_y : 0.022050\nupdate_stats check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 8 — TensorBoard Logging of the Initial Design (`_init_tensorboard()`)\n\n```python\nif self.tb_writer is not None:\n for i in range(len(self.y_)):\n self._write_tensorboard_hparams(self.X_[i], self.y_[i])\n self._write_tensorboard_scalars()\n```\n\n`_init_tensorboard()` logs each point of the initial design to TensorBoard as a\nseparate hyperparameter run, together with global scalar summaries.\nWhen `tensorboard_log=False` (the default), the writer is `None` and the method is a\nno-op with no runtime cost.\nWhen logging is enabled and no writer exists yet, `_init_tensorboard()` creates the\n`SummaryWriter`, choosing a timestamped directory if `tensorboard_path` was not\nspecified.\nThis lazy creation avoids producing stale log directories for runs that fail during\ninitialisation.\n\n::: {#fd187b5d .cell execution_count=8}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)], tensorboard_log=False)\nopt.optimize()\nassert opt.tb_writer is None\nprint(\"TensorBoard disabled: writer not created.\")\nprint(\"_init_tensorboard check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard disabled: writer not created.\n_init_tensorboard check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 9 — Initial Best (`get_best_xy_initial_design()`)\n\n```python\nbest_idx = np.argmin(self.y_)\nself.best_x_ = self.X_[best_idx].copy()\nself.best_y_ = self.y_[best_idx]\n```\n\nAfter the initial design is stored and its statistics computed, the point with the\nminimum objective value is identified and written to `best_x_` and `best_y_`.\nThese two attributes define the running best solution, updated by\n`_update_best_main_loop()` in every subsequent iteration.\nWhen `verbose=True`, the initial best is printed; for noisy problems the mean best\n(`min_mean_y`) is also reported alongside the raw minimum.\n\n::: {#d5254f12 .cell execution_count=9}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], n_initial=5, verbose=False)\nopt.X_ = np.array([[1.0, 2.0], [0.0, 0.0], [2.0, 1.0]])\nopt.y_ = np.array([5.0, 0.0, 5.0])\nopt.get_best_xy_initial_design()\nassert np.array_equal(opt.best_x_, [0.0, 0.0])\nassert opt.best_y_ == 0.0\nprint(f\"best_x_ : {opt.best_x_}\")\nprint(f\"best_y_ : {opt.best_y_}\")\nprint(\"get_best_xy_initial_design check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nbest_x_ : [0. 0.]\nbest_y_ : 0.0\nget_best_xy_initial_design check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 10 — Main Iteration Loop (`_run_sequential_loop()`)\n\n```python\nwhile len(self.y_) < effective_max_iter and \\\n time.time() < timeout_start + max_time * 60:\n self.n_iter_ += 1\n self.fit_scheduler()\n X_ocba = self.apply_ocba()\n x_next = self.suggest_next_infill_point()\n x_next_repeated = self.update_repeats_infill_points(x_next)\n if X_ocba is not None:\n x_next_repeated = append(X_ocba, x_next_repeated, axis=0)\n y_next = self.evaluate_function(x_next_repeated)\n x_next_repeated, y_next = self._handle_NA_new_points(x_next_repeated, y_next)\n self.update_success_rate(y_next)\n # restart check\n self.update_storage(x_next_repeated, y_next)\n self.update_stats()\n self._update_best_main_loop(x_next_repeated, y_next, start_time=timeout_start)\n```\n\n`_run_sequential_loop()` executes iterations until either the evaluation budget\n(`effective_max_iter`) or the wall-clock limit (`max_time` minutes) is exhausted.\nEach iteration increments `n_iter_`, fits the surrogate, selects and evaluates one or\nmore candidate points, and updates internal state.\nA safety counter tracks consecutive failures: more than `max_iter` consecutive\nNaN/inf evaluations triggers an early exit with `success=False`.\n\nThe loop returns `(\"RESTART\", result)` when `success_rate` has been zero for\n`restart_after_n` consecutive iterations, signalling `optimize()` to begin a fresh run.\nIt returns `(\"FINISHED\", result)` when the budget or time limit is reached.\n\n::: {#6950ad3f .cell execution_count=10}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=15, seed=0)\nresult = opt.optimize()\nprint(f\"iterations : {result.nit}\")\nprint(f\"evaluations : {result.nfev}\")\nprint(f\"best : {result.fun:.6f}\")\nassert result.nfev == 15\nassert result.success\nprint(\"_run_sequential_loop check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\niterations : 10\nevaluations : 15\nbest : 0.000001\n_run_sequential_loop check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 11 — Surrogate Fitting (`fit_scheduler()`)\n\n```python\nself.fit_scheduler()\n```\n\nAt the start of each iteration, the surrogate model is refitted to the current training\nwindow.\n`fit_scheduler()` selects the most recent `window_size` observations according to\n`selection_method` (default `\"distant\"`) and calls the surrogate's `fit()` method.\nWhen a list of surrogates was supplied at construction, one surrogate is chosen\nprobabilistically according to `prob_surrogate` before fitting, and per-surrogate\npoint caps from `_max_surrogate_points_list` are respected.\n\n::: {#33ed437b .cell execution_count=11}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=15, window_size=10, seed=0)\nresult = opt.optimize()\nprint(f\"window_size : {opt.window_size}\")\nprint(f\"evaluations : {result.nfev}\")\nassert opt.window_size == 10\nprint(\"fit_scheduler check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nwindow_size : 10\nevaluations : 15\nfit_scheduler check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 12 — OCBA Re-evaluations (`apply_ocba()`)\n\n```python\nX_ocba = self.apply_ocba()\n```\n\n`apply_ocba()` implements Optimal Computing Budget Allocation for noisy objective\nfunctions.\nWhen `ocba_delta > 0`, it identifies the `ocba_delta` best-mean points and schedules\nadditional evaluations at those locations, returning them as `X_ocba`.\nThese points are concatenated with the acquisition candidate before the objective call,\nso that noisy regions near the current optimum receive extra replication.\nWhen `ocba_delta == 0` (the default), the method returns `None` and adds no overhead.\n\n::: {#dbe4e77f .cell execution_count=12}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, ocba_delta=0, seed=0)\nresult = opt.optimize()\nassert opt.ocba_delta == 0\nprint(f\"ocba_delta : {opt.ocba_delta} (no OCBA overhead)\")\nprint(\"apply_ocba check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nocba_delta : 0 (no OCBA overhead)\napply_ocba check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 13 — Candidate Generation (`suggest_next_infill_point()`)\n\n```python\nx_next = self.suggest_next_infill_point()\n```\n\n`suggest_next_infill_point()` runs the acquisition function to identify the most\npromising candidate for the next objective evaluation.\nThe acquisition strategy is controlled by `acquisition` (default `\"y\"`, minimising the\nsurrogate prediction directly).\nThe acquisition optimiser (default `\"differential_evolution\"`) searches the\ntransformed, reduced search space using the bounds `[lower, upper]`.\nWhen the optimiser fails — no improvement found or a numerical issue — the strategy\nselected by `acquisition_failure_strategy` takes effect; `\"random\"` (the default)\ndraws a point uniformly at random.\n\n::: {#a9e5b7bc .cell execution_count=13}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, acquisition=\"ei\", seed=0)\nresult = opt.optimize()\nassert opt.acquisition == \"ei\"\nprint(f\"acquisition : {opt.acquisition}\")\nprint(f\"best : {result.fun:.6f}\")\nprint(\"suggest_next_infill_point check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nacquisition : ei\nbest : 0.304090\nsuggest_next_infill_point check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 14 — Repeat Infill Points (`update_repeats_infill_points()`)\n\n```python\nx_next_repeated = self.update_repeats_infill_points(x_next)\n```\n\nWhen `repeats_surrogate > 1`, each surrogate-suggested candidate is evaluated multiple\ntimes to reduce noise.\n`update_repeats_infill_points()` tiles the candidate point `repeats_surrogate` times,\nreturning a 2-D array of shape `(repeats_surrogate, n_dim)`.\nWhen `repeats_surrogate == 1` (the default) the array is simply `x_next` reshaped to\n`(1, n_dim)`, adding no computational cost.\n\n::: {#fdefa95a .cell execution_count=14}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, repeats_surrogate=1, seed=0)\nresult = opt.optimize()\nassert opt.repeats_surrogate == 1\nassert result.nfev == 10\nprint(f\"repeats_surrogate : {opt.repeats_surrogate}\")\nprint(\"update_repeats_infill_points check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nrepeats_surrogate : 1\nupdate_repeats_infill_points check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 15 — Objective Evaluation (`evaluate_function()`)\n\n```python\ny_next = self.evaluate_function(x_next_repeated)\n```\n\n`evaluate_function()` calls the user-supplied objective `fun` on the batch of\ncandidate points.\nThe input is always in transformed, reduced internal scale;\n`evaluate_function()` applies `inverse_transform_X()` and `to_all_dim()` before the\ncall, so `fun` always receives points in natural scale with all dimensions present.\nWhen `fun_mo2so` is set, the multi-objective output is first converted to a scalar\nusing that aggregation function.\nThe result is a 1-D array whose length equals the number of candidates evaluated.\n\n::: {#d05bdbe1 .cell execution_count=15}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, seed=0)\nresult = opt.optimize()\nassert len(opt.y_) == 10\nassert np.all(np.isfinite(opt.y_))\nprint(f\"total evaluations : {len(opt.y_)}\")\nprint(f\"best value : {opt.min_y:.6f}\")\nprint(\"evaluate_function check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ntotal evaluations : 10\nbest value : 0.022050\nevaluate_function check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 16 — NaN Handling for Sequential Evaluations (`_handle_NA_new_points()`)\n\n```python\nx_next_repeated, y_next = self._handle_NA_new_points(x_next_repeated, y_next)\n```\n\nUnlike the initial design (where invalid points are removed), NaN or inf values\nreturned during the sequential loop are replaced with a penalty derived from the worst\nfinite value seen so far, scaled by a large factor.\nThis preserves the storage structure — one row in `X_` per candidate — and prevents\nthe surrogate from ignoring pathological regions.\nIf every candidate in the batch is invalid, the method returns `(None, None)`, causing\nthe iteration to be skipped and the consecutive-failure counter to increment.\n\n::: {#ec6119d2 .cell execution_count=16}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=12, seed=0)\nopt.optimize()\nassert np.all(np.isfinite(opt.y_))\nprint(\"All stored values are finite after NaN handling.\")\nprint(\"_handle_NA_new_points check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nAll stored values are finite after NaN handling.\n_handle_NA_new_points check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 17 — Success Rate (`update_success_rate()`)\n\n```python\nself.update_success_rate(y_next)\n```\n\n`update_success_rate()` measures whether the current iteration produced an improvement\nrelative to `best_y_`.\nIt records a binary outcome in `_success_history` and computes `success_rate` as the\nfraction of recent iterations that showed improvement.\nWhen `success_rate` remains at `0.0` for `restart_after_n` consecutive iterations,\n`_zero_success_count` reaches the threshold and the loop returns `\"RESTART\"` to the\ncaller.\n\n::: {#0781bcc7 .cell execution_count=17}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=15, seed=0)\nopt.optimize()\nprint(f\"success_rate : {opt.success_rate:.4f}\")\nassert 0.0 <= opt.success_rate <= 1.0\nprint(\"update_success_rate check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nsuccess_rate : 0.8000\nupdate_success_rate check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 18 — Storage Update (`update_storage()`)\n\n```python\nself.update_storage(x_next_repeated, y_next)\n```\n\n`update_storage()` appends newly evaluated candidates to `X_` and `y_`.\nLike `init_storage()`, it converts points to natural scale via `inverse_transform_X()`\nbefore storing.\nAfter each call `X_.shape[0]` increases by the number of candidates evaluated — usually\n1, but more when `repeats_surrogate > 1` or OCBA is active.\nThe growing `X_` and `y_` arrays serve both as the surrogate training window and as\nthe final output embedded in `OptimizeResult`.\n\n::: {#ad782b1f .cell execution_count=18}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, seed=0)\nopt.optimize()\nassert opt.X_.shape == (10, 2)\nassert opt.y_.shape == (10,)\nprint(f\"X_ shape : {opt.X_.shape}\")\nprint(f\"y_ shape : {opt.y_.shape}\")\nprint(\"update_storage check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nX_ shape : (10, 2)\ny_ shape : (10,)\nupdate_storage check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 19 — Best Solution Update (`_update_best_main_loop()`)\n\n```python\nself._update_best_main_loop(x_next_repeated, y_next, start_time=timeout_start)\n```\n\nAt the end of each iteration, `_update_best_main_loop()` checks whether any of the\nnewly evaluated points improves on `best_y_`.\nIf so, `best_x_` and `best_y_` are updated in-place.\nWhen `verbose=True`, the improvement is printed together with elapsed time and the\ncurrent evaluation count.\nFor noisy problems, improvement is judged against `min_mean_y` rather than the raw\nminimum.\n\n::: {#b2d12bcf .cell execution_count=19}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=15, seed=0)\nresult = opt.optimize()\nassert np.isclose(opt.best_y_, result.fun)\nassert np.array_equal(opt.best_x_, result.x)\nprint(f\"best_x_ : {opt.best_x_}\")\nprint(f\"best_y_ : {opt.best_y_:.6f}\")\nprint(\"_update_best_main_loop check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nbest_x_ : [7.26480048e-04 3.90431069e-05]\nbest_y_ : 0.000001\n_update_best_main_loop check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 20 — Termination (`determine_termination()`)\n\n```python\nstatus_message = self.determine_termination(timeout_start)\n```\n\nAfter the while condition fails, `determine_termination()` produces the human-readable\ntermination message embedded in `OptimizeResult.message`.\nIt distinguishes three cases: the evaluation budget was exhausted\n(`nfev >= effective_max_iter`), the wall-clock limit was exceeded (`max_time`), or the\ntolerance criterion was met — consecutive best-point improvements smaller than\n`tolerance_x` measured by `min_tol_metric`.\nThe formatted message also includes the final function value, iteration count, and\ntotal evaluation count, matching the style of `scipy.optimize.minimize`.\n\n::: {#f400ccb0 .cell execution_count=20}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)],\n n_initial=5, max_iter=10, seed=0)\nresult = opt.optimize()\nfirst_line = result.message.splitlines()[0]\nprint(f\"termination : {first_line}\")\nassert \"10\" in result.message\nprint(\"determine_termination check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ntermination : Optimization terminated: maximum evaluations (10) reached\ndetermine_termination check passed.\n```\n:::\n:::\n\n\n---\n\n## Complete Sequential Run Summary\n\n@tbl-seq summarises every step executed along the sequential path in call order:\n\n| Step | Method | Purpose |\n|-----:|--------|---------|\n| 1 | `execute_optimization_run()` | Entry point, delegates to optimize_sequential_run() |\n| 2 | `optimize_sequential_run()` | Sequential orchestrator |\n| 3 | `_initialize_run()` | Seed RNG, generate and evaluate initial design |\n| 4 | `rm_initial_design_NA_values()` | Remove NaN/inf from initial evaluations |\n| 5 | `check_size_initial_design()` | Validate minimum initial design size |\n| 6 | `init_storage()` | Initialise `X_`, `y_`, `n_iter_` |\n| 7 | `update_stats()` | Compute `min_y`, `min_X`, `counter` |\n| 8 | `_init_tensorboard()` | Log initial design to TensorBoard |\n| 9 | `get_best_xy_initial_design()` | Identify initial `best_x_`, `best_y_` |\n| 10 | `_run_sequential_loop()` | Main iteration loop (Steps 11–20 per iteration) |\n| 11 | `fit_scheduler()` | Fit surrogate to current training window |\n| 12 | `apply_ocba()` | Schedule OCBA re-evaluations (noisy problems only) |\n| 13 | `suggest_next_infill_point()` | Optimise acquisition to propose candidate |\n| 14 | `update_repeats_infill_points()` | Replicate candidate for noisy evaluation |\n| 15 | `evaluate_function()` | Call `fun` in natural scale |\n| 16 | `_handle_NA_new_points()` | Penalise or skip invalid evaluations |\n| 17 | `update_success_rate()` | Track improvement rate; trigger restart if stalled |\n| 18 | `update_storage()` | Append candidates to `X_`, `y_` |\n| 19 | `update_stats()` | Refresh statistics after new evaluations |\n| 20 | `_update_best_main_loop()` | Update `best_x_`, `best_y_` |\n| 21 | `determine_termination()` | Produce termination message and `OptimizeResult` |\n\n: Complete Sequential Run Summary {#tbl-seq}\n\n", "supporting": [ "optimize_seq_files/figure-html" ], diff --git a/_freeze/docs/reference/SpotOptim.SpotOptim/execute-results/html.json b/_freeze/docs/reference/SpotOptim.SpotOptim/execute-results/html.json index c99bcb54..c626fd8e 100644 --- a/_freeze/docs/reference/SpotOptim.SpotOptim/execute-results/html.json +++ b/_freeze/docs/reference/SpotOptim.SpotOptim/execute-results/html.json @@ -1,10 +1,10 @@ { - "hash": "12d6cb0a656109880fa1f6816ab8d0ac", + "hash": "d6d91e86b2c9bc2fca7d9342c08683a1", "result": { "engine": "jupyter", - "markdown": "---\ntitle: SpotOptim.SpotOptim\n---\n\n\n\n```python\nSpotOptim.SpotOptim(\n fun,\n bounds=None,\n max_iter=20,\n n_initial=10,\n surrogate=None,\n acquisition='y',\n var_type=None,\n var_name=None,\n var_trans=None,\n tolerance_x=None,\n max_time=np.inf,\n repeats_initial=1,\n repeats_surrogate=1,\n ocba_delta=0,\n tensorboard_log=False,\n tensorboard_path=None,\n tensorboard_clean=False,\n fun_mo2so=None,\n seed=None,\n verbose=False,\n warnings_filter='ignore',\n n_infill_points=1,\n max_surrogate_points=None,\n selection_method='distant',\n acquisition_failure_strategy='random',\n penalty=False,\n penalty_val=None,\n acquisition_fun_return_size=3,\n acquisition_optimizer='differential_evolution',\n restart_after_n=100,\n restart_inject_best=True,\n x0=None,\n de_x0_prob=0.1,\n tricands_fringe=False,\n prob_de_tricands=0.8,\n window_size=None,\n min_tol_metric='chebyshev',\n prob_surrogate=None,\n n_jobs=1,\n eval_batch_size=1,\n acquisition_optimizer_kwargs=None,\n args=(),\n kwargs=None,\n)\n```\n\nSPOT optimizer compatible with scipy.optimize interface.\n\n## Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|------------------------------|-------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------|\n| fun | [callable](`callable`) | Objective function to minimize. Should accept array of shape (n_samples, n_features). | _required_ |\n| bounds | list of tuple | Bounds for each dimension as [(low, high), ...]. | `None` |\n| max_iter | [int](`int`) | Maximum number of total function evaluations (including initial design). For example, max_iter=30 with n_initial=10 will perform 10 initial evaluations plus 20 sequential optimization iterations. Defaults to 20. | `20` |\n| n_initial | [int](`int`) | Number of initial design points. Defaults to 10. | `10` |\n| surrogate | [object](`object`) | Surrogate model with scikit-learn interface (fit/predict methods). If None, uses a Gaussian Process Regressor with Matern kernel. Default configuration:: * `from sklearn.gaussian_process import GaussianProcessRegressor` * `from sklearn.gaussian_process.kernels import Matern, ConstantKernel` * `kernel = ConstantKernel(1.0, (1e-2, 1e12)) * Matern(length_scale=1.0, length_scale_bounds=(1e-4, 1e2), nu=2.5)` * `surrogate = GaussianProcessRegressor(kernel=kernel, n_restarts_optimizer=100)` Alternative surrogates can be provided, including SpotOptim's Kriging model, Random Forests, or any scikit-learn compatible regressor. See Examples section. Defaults to None (uses default Gaussian Process configuration). | `None` |\n| acquisition | [str](`str`) | Acquisition function ('ei', 'y', 'pi'). Defaults to 'y'. | `'y'` |\n| var_type | list of str | Variable types for each dimension. Supported types: * 'float': Python floats, continuous optimization (no rounding) * 'int': Python int, float values will be rounded to integers * 'factor': Unordered categorical data, internally mapped to int values (e.g., \"red\"->0, \"green\"->1, etc.) Defaults to None (which sets all dimensions to 'float'). | `None` |\n| var_name | list of str | Variable names for each dimension. If None, uses default names ['x0', 'x1', 'x2', ...]. Defaults to None. | `None` |\n| tolerance_x | [float](`float`) | Minimum distance between points. Defaults to np.sqrt(np.spacing(1)) | `None` |\n| var_trans | list of str | Variable transformations for each dimension. Supported: It can be one of `id`, `log10`, `log`, `ln`, `sqrt`, `exp`, `square`, `cube`, `inv`, `reciprocal`, or `None`. Also supports dynamic strings like `log(x)`, `sqrt(x)`, `pow(x, p)`. Defaults to None (no transformations). | `None` |\n| max_time | [float](`float`) | Maximum runtime in minutes. If np.inf (default), no time limit. The optimization terminates when either max_iter evaluations are reached OR max_time minutes have elapsed, whichever comes first. Defaults to np.inf. | `np.inf` |\n| repeats_initial | [int](`int`) | Number of times to evaluate each initial design point. Useful for noisy objective functions. If > 1, noise handling is activated and statistics (mean, variance) are tracked. Defaults to 1. | `1` |\n| repeats_surrogate | [int](`int`) | Number of times to evaluate each surrogate-suggested point. Useful for noisy objective functions. If > 1, noise handling is activated and statistics (mean, variance) are tracked. Defaults to 1. | `1` |\n| ocba_delta | [int](`int`) | Number of additional evaluations to allocate using Optimal Computing Budget Allocation (OCBA) when noise handling is active. OCBA determines which existing design points should be re-evaluated to best distinguish between alternatives. Only used when repeats_surrogate > 1 and ocba_delta > 0. Requires at least 3 design points with variance information. Defaults to 0 (no OCBA). | `0` |\n| tensorboard_log | [bool](`bool`) | Enable TensorBoard logging. If True, optimization metrics and hyperparameters are logged to TensorBoard. View logs by running: `tensorboard --logdir=` in a separate terminal. Defaults to False. | `False` |\n| tensorboard_path | [str](`str`) | Path for TensorBoard log files. If None and tensorboard_log is True, creates a default path: runs/spotoptim_YYYYMMDD_HHMMSS. Defaults to None. | `None` |\n| tensorboard_clean | [bool](`bool`) | If True, removes all old TensorBoard log directories from the 'runs' folder before starting optimization. Use with caution as this permanently deletes all subdirectories in 'runs'. Defaults to False. | `False` |\n| fun_mo2so | [callable](`callable`) | Function to convert multi-objective values to single-objective. Takes an array of shape (n_samples, n_objectives) and returns array of shape (n_samples,). If None and objective function returns multi-objective values, uses first objective. Defaults to None. | `None` |\n| seed | [int](`int`) | Random seed for reproducibility. Defaults to None. | `None` |\n| verbose | [bool](`bool`) | Print progress information. Defaults to False. | `False` |\n| warnings_filter | [Literal](`typing.Literal`)\\[\\'default\\', \\'error\\', \\'ignore\\'\\] | Filter for warnings. One of \"error\", \"ignore\", \"always\", \"all\", \"default\", \"module\", or \"once\". Defaults to \"ignore\". | `'ignore'` |\n| n_infill_points | [int](`int`) | Number of infill points to suggest at each iteration. Defaults to 1. If > 1, multiple distinct points are proposed using the optimizer and fallback strategies. | `1` |\n| max_surrogate_points | [int](`int`) | Maximum number of points to use for surrogate model fitting. If None, all points are used. If the number of evaluated points exceeds this limit, a subset is selected using the selection method. Defaults to None. | `None` |\n| selection_method | [str](`str`) | Method for selecting points when max_surrogate_points is exceeded. Options: 'distant' (Select points that are distant from each other via K-means clustering) or 'best' (Select all points from the cluster with the best mean objective value). Defaults to 'distant'. | `'distant'` |\n| acquisition_failure_strategy | [str](`str`) | Strategy for handling acquisition function failures. Options: 'random' (space-filling design via Latin Hypercube Sampling) Defaults to 'random'. | `'random'` |\n| penalty | [bool](`bool`) | Whether to use penalty for handling NaN/inf values in objective function evaluations. Defaults to False. | `False` |\n| penalty_val | [float](`float`) | Penalty value to replace NaN/inf values in objective function evaluations. When the objective function returns NaN or inf, these values are replaced with penalty plus a small random noise (sampled from N(0, 0.1)) to avoid identical penalty values. This allows optimization to continue despite occasional function evaluation failures. Defaults to None. | `None` |\n| acquisition_fun_return_size | [int](`int`) | Number of top candidates to return from acquisition function optimization. Defaults to 3. | `3` |\n| acquisition_optimizer | [str](`str`) or [callable](`callable`) | Optimizer to use for maximizing acquisition function. Can be \"differential_evolution\" (default) or any method name supported by scipy.optimize.minimize (e.g., \"Nelder-Mead\", \"L-BFGS-B\"). Can also be a callable with signature compatible with scipy.optimize.minimize (fun, x0, bounds, ...). A specific version is \"de_tricands\", which combines DE with Tricands. It can be parameterized with \"prob_de_tricands\" (probability of using DE). Defaults to \"differential_evolution\". | `'differential_evolution'` |\n| acquisition_optimizer_kwargs | [dict](`dict`) | Kwargs passed to the acquisition function optimizer and GPR surrogate optimizer. Defaults to {'maxiter': 10000, 'gtol': 1e-9}. | `None` |\n| restart_after_n | [int](`int`) | Number of consecutive iterations with zero success rate before triggering a restart. Defaults to 100. | `100` |\n| restart_inject_best | [bool](`bool`) | Whether to inject the best solution found so far as a starting point for the next restart. Defaults to True. | `True` |\n| x0 | [array](`array`) - [like](`like`) | Starting point for optimization, shape (n_features,). If provided, this point will be evaluated first and included in the initial design. The point should be within the bounds and will be validated before use. Defaults to None (no starting point, uses only LHS design). | `None` |\n| de_x0_prob | [float](`float`) | Probability of using the best point as starting point for differential evolution. Defaults to 0.1. | `0.1` |\n| tricands_fringe | [bool](`bool`) | Whether to use the fringe of the design space for the initial design. Defaults to False. | `False` |\n| prob_de_tricands | [float](`float`) | Probability of using differential evolution as an optimizer on the surrogate model. 1 - prob_de_tricands is the probability of using tricands. Defaults to 0.8. | `0.8` |\n| n_jobs | [int](`int`) | Number of parallel workers. ``1`` (default) runs sequentially. Values ``> 1`` activate steady-state parallel optimization: objective evaluations and acquisition searches are dispatched across ``n_jobs`` processes. Pass ``-1`` to use all available CPU cores (``os.cpu_count()``). ``0`` and values ``< -1`` raise ``ValueError``. Defaults to ``1``. | `1` |\n| eval_batch_size | [int](`int`) | Number of candidate points gathered from search tasks before a single ``fun(X_batch)`` call is dispatched to the process pool. ``1`` (default) preserves one-point-per-call behavior. Set to ``n_jobs`` or higher to exploit vectorized objective functions and reduce process-spawn overhead. Ignored when ``n_jobs == 1``. Must be ``>= 1``. Defaults to ``1``. | `1` |\n| window_size | [int](`int`) | Window size for success rate calculation. | `None` |\n| min_tol_metric | [str](`str`) | Distance metric used when checking `tolerance_x` for duplicate detection. Default is \"chebyshev\". Supports all metrics from scipy.spatial.distance.cdist, including: * \"chebyshev\": L-infinity distance (hypercube). Default. Matches previous behavior. * \"euclidean\": L2 distance (hypersphere). * \"minkowski\": Lp distance (default p=2). * \"cityblock\": Manhattan/L1 distance. * \"cosine\": Cosine distance. * \"correlation\": Correlation distance. * \"canberra\", \"braycurtis\", \"sqeuclidean\", etc. | `'chebyshev'` |\n\n## Attributes {.doc-section .doc-section-attributes}\n\n| Name | Type | Description |\n|------------------------------|-------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|\n| X_ | [ndarray](`ndarray`) | All evaluated points, shape (n_samples, n_features). |\n| y_ | [ndarray](`ndarray`) | Function values at X_, shape (n_samples,). For multi-objective problems, these are the converted single-objective values. |\n| y_mo | [ndarray](`ndarray`) or None | Multi-objective function values, shape (n_samples, n_objectives). None for single-objective problems. |\n| best_x_ | [ndarray](`ndarray`) | Best point found, shape (n_features,). |\n| best_y_ | [float](`float`) | Best function value found. |\n| n_iter_ | [int](`int`) | Number of iterations performed. This is not the same as counter. Provided for compatibility with scipy.optimize routines. |\n| counter | [int](`int`) | Total number of function evaluations. |\n| success_rate | [float](`float`) | Rolling success rate over the last window_size evaluations. A success is counted when a new evaluation improves upon the best value found so far. |\n| warnings_filter | [Literal](`typing.Literal`)\\[\\'default\\', \\'error\\', \\'ignore\\'\\] | Filter for warnings during optimization. |\n| max_surrogate_points | [int](`int`) or None | Maximum number of points for surrogate fitting. |\n| selection_method | [str](`str`) | Point selection method. |\n| acquisition_failure_strategy | [str](`str`) | Strategy for handling acquisition failures ('random'). |\n| mean_X | [ndarray](`ndarray`) or None | Aggregated unique design points (if repeats_surrogate > 1). |\n| mean_y | [ndarray](`ndarray`) or None | Mean y values per design point (if repeats_surrogate > 1). |\n| var_y | [ndarray](`ndarray`) or None | Variance of y values per design point (if repeats_surrogate > 1). |\n| min_mean_X | [ndarray](`ndarray`) or None | X value of best mean y (if repeats_surrogate > 1). |\n| min_mean_y | [float](`float`) or None | Best mean y value (if repeats_surrogate > 1). |\n| min_var_y | [float](`float`) or None | Variance of best mean y (if repeats_surrogate > 1). |\n| de_x0_prob | [float](`float`) | Probability of using the best point as starting point for differential evolution. |\n| tricands_fringe | [bool](`bool`) | Whether to use the fringe of the design space for the initial design. |\n| prob_de_tricands | [float](`float`) | Probability of using differential evolution as an optimizer on the surrogate model. |\n\n## Examples {.doc-section .doc-section-examples}\n\n\n::: {#830e1601 .cell execution_count=1}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 1: Basic usage (deterministic function)\nbounds = [(-5, 5), (-5, 5)]\noptimizer = SpotOptim(fun=objective, bounds=bounds, max_iter=10, n_initial=5, verbose=True)\nresult = optimizer.optimize()\nprint(\"Best x:\", result.x)\nprint(\"Best f(x):\", result.fun)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 9.487810\nIter 1 | Best: 9.487810 | Curr: 50.000000 | Rate: 0.00 | Evals: 60.0%\nIter 2 | Best: 7.863616 | Rate: 0.50 | Evals: 70.0%\nIter 3 | Best: 3.062302 | Rate: 0.67 | Evals: 80.0%\nIter 4 | Best: 0.251210 | Rate: 0.75 | Evals: 90.0%\nIter 5 | Best: 0.085451 | Rate: 0.80 | Evals: 100.0%\nBest x: [0.02429731 0.29130892]\nBest f(x): 0.08545124721496651\n```\n:::\n:::\n\n\n::: {#dfa8309f .cell execution_count=2}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 2: With custom variable names\noptimizer = SpotOptim(\n fun=objective,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"param1\", \"param2\"],\n max_iter=10,\n n_initial=5\n)\nresult = optimizer.optimize()\n# Ensure we can use custom names in plots\noptimizer.plot_surrogate(show=False)\n```\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim.SpotOptim_files/figure-html/cell-3-output-1.png){width=1125 height=950}\n:::\n:::\n\n\n::: {#ae69c8b5 .cell execution_count=3}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\n# Example 3: Noisy function with repeated evaluations\ndef noisy_objective(X):\n base = np.sum(X**2, axis=1)\n noise = np.random.normal(0, 0.1, size=base.shape)\n return base + noise\n\noptimizer = SpotOptim(\n fun=noisy_objective,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5,\n repeats_initial=1, # Evaluate each initial point once\n repeats_surrogate=2, # Evaluate each new point twice\n seed=42, # For reproducibility\n verbose=True\n)\nresult = optimizer.optimize()\n\n# Access noise statistics\nprint(\"Unique design points:\", optimizer.mean_X.shape[0])\nprint(\"Best mean value:\", optimizer.min_mean_y)\nprint(\"Variance at best point:\", optimizer.min_var_y)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 3.403652, mean best: f(x) = 3.403652\nIter 1 | Best: 3.279049 | Rate: 0.50 | Evals: 70.0% | Mean Best: 3.369716\nIter 2 | Best: 3.279049 | Curr: 3.392849 | Rate: 0.25 | Evals: 90.0% | Mean Curr: 3.454694\nIter 3 | Best: 1.563282 | Rate: 0.50 | Evals: 110.0% | Mean Best: 1.613581\nUnique design points: 8\nBest mean value: 1.6135806237005457\nVariance at best point: 0.002529978015323257\n```\n:::\n:::\n\n\n::: {#d163ac02 .cell execution_count=4}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef noisy_objective(X):\n base = np.sum(X**2, axis=1)\n noise = np.random.normal(0, 0.1, size=base.shape)\n return base + noise\n\n# Example 4: Noisy function with OCBA (Optimal Computing Budget Allocation)\noptimizer_ocba = SpotOptim(\n fun=noisy_objective,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=20,\n n_initial=5,\n repeats_initial=2, # Initial repeats\n repeats_surrogate=1, # Surrogate repeats\n ocba_delta=3, # Allocate 3 additional evaluations per iteration\n seed=42,\n verbose=True\n)\nresult = optimizer_ocba.optimize()\n\n# OCBA intelligently re-evaluates promising points to reduce uncertainty\nprint(\"Total evaluations:\", result.nfev)\nprint(\"Unique design points:\", optimizer_ocba.mean_X.shape[0])\nprint(\"Best mean value:\", optimizer_ocba.min_mean_y)\nprint(\"Variance at best point:\", optimizer_ocba.min_var_y)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 3.328092, mean best: f(x) = 3.368681\n\nIn get_ocba():\nmeans: [25.90094202 19.61660056 23.96405211 3.36868097 10.79578138]\nvars: [6.73858271e-13 2.56053422e-03 1.00799409e-03 1.64745915e-03\n 1.91555606e-03]\ndelta: 3\nn_designs: 5\nRatios: [3.82210611e-11 2.79305049e-01 6.84325095e-02 9.58065217e-01\n 1.00000000e+00]\nBest: 3, Second best: 4\n OCBA: Adding 3 re-evaluation(s)\nIter 1 | Best: 3.103418 | Rate: 0.75 | Evals: 70.0% | Mean Best: 3.103418\nIter 2 | Best: 3.103418 | Curr: 3.354605 | Rate: 0.60 | Evals: 75.0% | Mean Curr: 3.354605\nIter 3 | Best: 1.613729 | Rate: 0.67 | Evals: 80.0% | Mean Best: 1.613729\nIter 4 | Best: 1.230181 | Rate: 0.71 | Evals: 85.0% | Mean Best: 1.230181\nIter 5 | Best: 0.449320 | Rate: 0.75 | Evals: 90.0% | Mean Best: 0.449320\nIter 6 | Best: 0.367163 | Rate: 0.78 | Evals: 95.0% | Mean Best: 0.367163\nIter 7 | Best: 0.367163 | Curr: 0.518496 | Rate: 0.70 | Evals: 100.0% | Mean Curr: 0.518496\nTotal evaluations: 20\nUnique design points: 12\nBest mean value: 0.3671633104119547\nVariance at best point: 0.0\n```\n:::\n:::\n\n\n::: {#79dfc5ac .cell execution_count=5}\n``` {.python .cell-code}\nimport numpy as np\nimport shutil\nimport os\nfrom spotoptim import SpotOptim\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 5: With TensorBoard logging\ntb_dir = \"runs/my_optimization\"\noptimizer_tb = SpotOptim(\n fun=objective,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5,\n tensorboard_log=True, # Enable TensorBoard\n tensorboard_path=tb_dir, # Optional custom path\n verbose=True\n)\nresult = optimizer_tb.optimize()\n\n# View logs in browser: tensorboard --logdir=runs/my_optimization\nprint(\"Logs saved to:\", optimizer_tb.tensorboard_path)\n\n# Cleanup log dir\nif os.path.exists(tb_dir):\n shutil.rmtree(tb_dir)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging enabled: runs/my_optimization\nInitial best: f(x) = 6.390078\nOptimizer candidate 1/3 was duplicate/invalid.\nIter 1 | Best: 6.383435 | Rate: 1.00 | Evals: 60.0%\nIter 2 | Best: 5.402011 | Rate: 1.00 | Evals: 70.0%\nIter 3 | Best: 2.814954 | Rate: 1.00 | Evals: 80.0%\nIter 4 | Best: 1.164272 | Rate: 1.00 | Evals: 90.0%\nIter 5 | Best: 0.269468 | Rate: 1.00 | Evals: 100.0%\nTensorBoard writer closed. View logs with: tensorboard --logdir=runs/my_optimization\nLogs saved to: runs/my_optimization\n```\n:::\n:::\n\n\n::: {#5dff3806 .cell execution_count=6}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.surrogate import Kriging\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 6: Using SpotOptim's Kriging surrogate\nkriging_model = Kriging(\n noise=1e-10, # Regularization parameter\n kernel='gauss', # Gaussian/RBF kernel\n min_theta=-3.0, # Min log10(theta) bound\n max_theta=2.0, # Max log10(theta) bound\n seed=42\n)\noptimizer_kriging = SpotOptim(\n fun=objective,\n bounds=[(-5, 5), (-5, 5)],\n surrogate=kriging_model,\n max_iter=10,\n n_initial=5,\n seed=42,\n verbose=True\n)\nresult = optimizer_kriging.optimize()\nprint(\"Best solution found:\", result.x)\nprint(\"Best value:\", result.fun)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 3.251349\nIter 1 | Best: 3.251349 | Curr: 4.425619 | Rate: 0.00 | Evals: 60.0%\nIter 2 | Best: 1.617693 | Rate: 0.50 | Evals: 70.0%\nIter 3 | Best: 1.617693 | Curr: 18.716279 | Rate: 0.33 | Evals: 80.0%\nIter 4 | Best: 0.839564 | Rate: 0.50 | Evals: 90.0%\nIter 5 | Best: 0.102879 | Rate: 0.60 | Evals: 100.0%\nBest solution found: [0.00128033 0.32074518]\nBest value: 0.10287911055669191\n```\n:::\n:::\n\n\n::: {#655445d4 .cell execution_count=7}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom sklearn.gaussian_process import GaussianProcessRegressor\nfrom sklearn.gaussian_process.kernels import RBF, ConstantKernel, WhiteKernel\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 7: Using sklearn Gaussian Process with custom kernel\n# Custom kernel: constant * RBF + white noise\ncustom_kernel = ConstantKernel(1.0, (1e-2, 1e2)) * RBF(\n length_scale=1.0, length_scale_bounds=(1e-1, 10.0)\n) + WhiteKernel(noise_level=1e-5, noise_level_bounds=(1e-10, 1e-1))\n\ngp_custom = GaussianProcessRegressor(\n kernel=custom_kernel,\n n_restarts_optimizer=15,\n normalize_y=True,\n random_state=42\n)\n\noptimizer_custom_gp = SpotOptim(\n fun=objective,\n bounds=[(-5, 5), (-5, 5)],\n surrogate=gp_custom,\n max_iter=10,\n n_initial=5,\n seed=42\n)\nresult = optimizer_custom_gp.optimize()\n```\n:::\n\n\n::: {#3ea28ad4 .cell execution_count=8}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom sklearn.ensemble import RandomForestRegressor\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 8: Using Random Forest as surrogate\nrf_model = RandomForestRegressor(\n n_estimators=100,\n max_depth=10,\n random_state=42\n)\n\noptimizer_rf = SpotOptim(\n fun=objective,\n bounds=[(-5, 5), (-5, 5)],\n surrogate=rf_model,\n max_iter=10,\n n_initial=5,\n seed=42\n)\nresult = optimizer_rf.optimize()\n\n# Note: Random Forests don't provide uncertainty estimates,\n# so Expected Improvement (EI) may be less effective.\n# Consider using acquisition='y' for pure exploitation.\n```\n:::\n\n\n::: {#86264150 .cell execution_count=9}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom sklearn.gaussian_process import GaussianProcessRegressor\nfrom sklearn.gaussian_process.kernels import Matern, RationalQuadratic, ConstantKernel, RBF\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 9: Comparing different kernels for Gaussian Process\n# Matern kernel with nu=1.5 (once differentiable)\nkernel_matern15 = ConstantKernel(1.0) * Matern(length_scale=1.0, nu=1.5)\ngp_matern15 = GaussianProcessRegressor(kernel=kernel_matern15, normalize_y=True)\n\n# Matern kernel with nu=2.5 (twice differentiable, DEFAULT)\nkernel_matern25 = ConstantKernel(1.0) * Matern(length_scale=1.0, nu=2.5)\ngp_matern25 = GaussianProcessRegressor(kernel=kernel_matern25, normalize_y=True)\n\n# RBF kernel (infinitely differentiable, smooth)\nkernel_rbf = ConstantKernel(1.0) * RBF(length_scale=1.0)\ngp_rbf = GaussianProcessRegressor(kernel=kernel_rbf, normalize_y=True)\n\n# Rational Quadratic kernel (mixture of RBF kernels)\nkernel_rq = ConstantKernel(1.0) * RationalQuadratic(length_scale=1.0, alpha=1.0)\ngp_rq = GaussianProcessRegressor(kernel=kernel_rq, normalize_y=True)\n\n# Use any of these as surrogate\noptimizer_rbf = SpotOptim(fun=objective, bounds=[(-5, 5), (-5, 5)],\n surrogate=gp_rbf, max_iter=10, n_initial=5)\nresult = optimizer_rbf.optimize()\n```\n:::\n\n\n## Methods\n\n| Name | Description |\n| --- | --- |\n| [aggregate_mean_var](#spotoptim.SpotOptim.SpotOptim.aggregate_mean_var) | Aggregate X and y values to compute mean and variance per group. |\n| [apply_ocba](#spotoptim.SpotOptim.SpotOptim.apply_ocba) | Apply Optimal Computing Budget Allocation for noisy functions. |\n| [apply_penalty_NA](#spotoptim.SpotOptim.SpotOptim.apply_penalty_NA) | Replace NaN and infinite values with penalty plus random noise. |\n| [check_size_initial_design](#spotoptim.SpotOptim.SpotOptim.check_size_initial_design) | Validate that initial design has sufficient points for surrogate fitting. |\n| [curate_initial_design](#spotoptim.SpotOptim.SpotOptim.curate_initial_design) | Remove duplicates and ensure sufficient unique points in initial design. |\n| [detect_var_type](#spotoptim.SpotOptim.SpotOptim.detect_var_type) | Auto-detect variable types based on factor mappings. |\n| [determine_termination](#spotoptim.SpotOptim.SpotOptim.determine_termination) | Determine termination reason for optimization. |\n| [evaluate_function](#spotoptim.SpotOptim.SpotOptim.evaluate_function) | Evaluate objective function at points X. |\n| [execute_optimization_run](#spotoptim.SpotOptim.SpotOptim.execute_optimization_run) | Dispatcher for optimization run (Sequential vs Steady-State Parallel). |\n| [fit_scheduler](#spotoptim.SpotOptim.SpotOptim.fit_scheduler) | Fit surrogate model using appropriate data based on noise handling. |\n| [fit_select_best_cluster](#spotoptim.SpotOptim.SpotOptim.fit_select_best_cluster) | Selects all points from the cluster with the smallest mean y value. |\n| [fit_select_distant_points](#spotoptim.SpotOptim.SpotOptim.fit_select_distant_points) | Selects k points that are distant from each other using K-means clustering. |\n| [fit_selection_dispatcher](#spotoptim.SpotOptim.SpotOptim.fit_selection_dispatcher) | Dispatcher for selection methods. |\n| [fit_surrogate](#spotoptim.SpotOptim.SpotOptim.fit_surrogate) | Fit surrogate model to data. |\n| [gen_design_table](#spotoptim.SpotOptim.SpotOptim.gen_design_table) | Generate a table of the design or results. |\n| [generate_initial_design](#spotoptim.SpotOptim.SpotOptim.generate_initial_design) | Generate initial space-filling design using Latin Hypercube Sampling. |\n| [get_best_hyperparameters](#spotoptim.SpotOptim.SpotOptim.get_best_hyperparameters) | Get the best hyperparameter configuration found during optimization. |\n| [get_best_xy_initial_design](#spotoptim.SpotOptim.SpotOptim.get_best_xy_initial_design) | Determine and store the best point from initial design. |\n| [get_design_table](#spotoptim.SpotOptim.SpotOptim.get_design_table) | Get a table string showing the search space design before optimization. |\n| [get_experiment_filename](#spotoptim.SpotOptim.SpotOptim.get_experiment_filename) | Generate experiment filename with '_exp.pkl' suffix. |\n| [get_importance](#spotoptim.SpotOptim.SpotOptim.get_importance) | Calculate variable importance scores. |\n| [get_initial_design](#spotoptim.SpotOptim.SpotOptim.get_initial_design) | Generate or process initial design points. Ensures that design points are in |\n| [get_ocba](#spotoptim.SpotOptim.SpotOptim.get_ocba) | Optimal Computing Budget Allocation (OCBA). |\n| [get_ocba_X](#spotoptim.SpotOptim.SpotOptim.get_ocba_X) | Calculate OCBA allocation and repeat input array X. |\n| [get_pickle_safe_optimizer](#spotoptim.SpotOptim.SpotOptim.get_pickle_safe_optimizer) | Create a pickle-safe copy of the optimizer. |\n| [get_ranks](#spotoptim.SpotOptim.SpotOptim.get_ranks) | Returns ranks of numbers within input array x. |\n| [get_result_filename](#spotoptim.SpotOptim.SpotOptim.get_result_filename) | Generate result filename with '_res.pkl' suffix. |\n| [get_results_table](#spotoptim.SpotOptim.SpotOptim.get_results_table) | Get a comprehensive table string of optimization results. |\n| [get_shape](#spotoptim.SpotOptim.SpotOptim.get_shape) | Get the shape of the objective function output. |\n| [get_stars](#spotoptim.SpotOptim.SpotOptim.get_stars) | Converts a list of values to a list of stars. |\n| [get_success_rate](#spotoptim.SpotOptim.SpotOptim.get_success_rate) | Get the current success rate of the optimization process. |\n| [handle_default_var_trans](#spotoptim.SpotOptim.SpotOptim.handle_default_var_trans) | Handle default variable transformations. Does not perform any transformations, |\n| [init_storage](#spotoptim.SpotOptim.SpotOptim.init_storage) | Initialize storage for optimization. |\n| [init_surrogate](#spotoptim.SpotOptim.SpotOptim.init_surrogate) | Initialize or configure the surrogate model for optimization. Handles three surrogate configurations: |\n| [inverse_transform_X](#spotoptim.SpotOptim.SpotOptim.inverse_transform_X) | Transform parameter array from internal to original scale. |\n| [inverse_transform_value](#spotoptim.SpotOptim.SpotOptim.inverse_transform_value) | Apply inverse transformation to a single float value. |\n| [load_experiment](#spotoptim.SpotOptim.SpotOptim.load_experiment) | Load experiment configuration from a pickle file. |\n| [load_result](#spotoptim.SpotOptim.SpotOptim.load_result) | Load complete optimization results from a pickle file. |\n| [map_to_factor_values](#spotoptim.SpotOptim.SpotOptim.map_to_factor_values) | Map internal integer factor values back to string labels. |\n| [mo2so](#spotoptim.SpotOptim.SpotOptim.mo2so) | Convert multi-objective values to single-objective. |\n| [modify_bounds_based_on_var_type](#spotoptim.SpotOptim.SpotOptim.modify_bounds_based_on_var_type) | Modify bounds based on variable types. |\n| [optimize](#spotoptim.SpotOptim.SpotOptim.optimize) | Run the optimization process. The optimization terminates when either the total function evaluations reach |\n| [optimize_acquisition_func](#spotoptim.SpotOptim.SpotOptim.optimize_acquisition_func) | Optimize the acquisition function to find the next point to evaluate. |\n| [optimize_sequential_run](#spotoptim.SpotOptim.SpotOptim.optimize_sequential_run) | Perform a single sequential optimization run. |\n| [optimize_steady_state](#spotoptim.SpotOptim.SpotOptim.optimize_steady_state) | Perform steady-state asynchronous optimization (n_jobs > 1). |\n| [plot_importance](#spotoptim.SpotOptim.SpotOptim.plot_importance) | Plot variable importance. |\n| [plot_important_hyperparameter_contour](#spotoptim.SpotOptim.SpotOptim.plot_important_hyperparameter_contour) | Plot surrogate contours using spotoptim.plot.visualization.plot_important_hyperparameter_contour. |\n| [plot_parameter_scatter](#spotoptim.SpotOptim.SpotOptim.plot_parameter_scatter) | Plot parameter distributions showing relationship between each parameter and objective. |\n| [plot_progress](#spotoptim.SpotOptim.SpotOptim.plot_progress) | Plot optimization progress using spotoptim.plot.visualization.plot_progress. |\n| [plot_surrogate](#spotoptim.SpotOptim.SpotOptim.plot_surrogate) | Plot the surrogate model for two dimensions. |\n| [print_best](#spotoptim.SpotOptim.SpotOptim.print_best) | Print the best solution found during optimization. |\n| [print_results](#spotoptim.SpotOptim.SpotOptim.print_results) | Alias for print(get_results_table()) for compatibility. |\n| [process_factor_bounds](#spotoptim.SpotOptim.SpotOptim.process_factor_bounds) | Process `bounds` to handle factor variables. |\n| [reinitialize_components](#spotoptim.SpotOptim.SpotOptim.reinitialize_components) | Reinitialize components that were excluded during pickling. |\n| [remove_nan](#spotoptim.SpotOptim.SpotOptim.remove_nan) | Remove rows where y contains NaN or inf values. |\n| [repair_non_numeric](#spotoptim.SpotOptim.SpotOptim.repair_non_numeric) | Round non-numeric values to integers based on variable type. |\n| [rm_initial_design_NA_values](#spotoptim.SpotOptim.SpotOptim.rm_initial_design_NA_values) | Remove NaN/inf values from initial design evaluations. |\n| [save_experiment](#spotoptim.SpotOptim.SpotOptim.save_experiment) | Save experiment configuration to a pickle file. |\n| [save_result](#spotoptim.SpotOptim.SpotOptim.save_result) | Save complete optimization results to a pickle file. |\n| [select_new](#spotoptim.SpotOptim.SpotOptim.select_new) | Select rows from A that are not in X. |\n| [sensitivity_spearman](#spotoptim.SpotOptim.SpotOptim.sensitivity_spearman) | Compute and print Spearman correlation between parameters and objective values. |\n| [set_seed](#spotoptim.SpotOptim.SpotOptim.set_seed) | Set global random seeds for reproducibility. |\n| [setup_dimension_reduction](#spotoptim.SpotOptim.SpotOptim.setup_dimension_reduction) | Set up dimension reduction by identifying fixed dimensions. |\n| [store_mo](#spotoptim.SpotOptim.SpotOptim.store_mo) | Store multi-objective values in self.y_mo. |\n| [suggest_next_infill_point](#spotoptim.SpotOptim.SpotOptim.suggest_next_infill_point) | Suggest next point to evaluate (dispatcher). |\n| [to_all_dim](#spotoptim.SpotOptim.SpotOptim.to_all_dim) | Expand reduced-dimensional points to full-dimensional representation. |\n| [to_red_dim](#spotoptim.SpotOptim.SpotOptim.to_red_dim) | Reduce full-dimensional points to optimization space. |\n| [transform_X](#spotoptim.SpotOptim.SpotOptim.transform_X) | Transform parameter array from original (natural) to internal scale. |\n| [transform_bounds](#spotoptim.SpotOptim.SpotOptim.transform_bounds) | Transform bounds from original to internal scale. |\n| [transform_value](#spotoptim.SpotOptim.SpotOptim.transform_value) | Apply transformation to a single float value. |\n| [update_repeats_infill_points](#spotoptim.SpotOptim.SpotOptim.update_repeats_infill_points) | Repeat infill point for noisy function evaluation. Used in the sequential_loop. |\n| [update_stats](#spotoptim.SpotOptim.SpotOptim.update_stats) | Update optimization statistics. |\n| [update_storage](#spotoptim.SpotOptim.SpotOptim.update_storage) | Update storage (`X_`, `y_`) with new evaluation points. |\n| [update_success_rate](#spotoptim.SpotOptim.SpotOptim.update_success_rate) | Update the rolling success rate of the optimization process. |\n| [validate_x0](#spotoptim.SpotOptim.SpotOptim.validate_x0) | Validate and process starting point x0. Called in `__init__` and `optimize`. |\n\n### aggregate_mean_var { #spotoptim.SpotOptim.SpotOptim.aggregate_mean_var }\n\n```python\nSpotOptim.SpotOptim.aggregate_mean_var(X, y)\n```\n\nAggregate X and y values to compute mean and variance per group.\nFor repeated evaluations at the same design point, this method computes\nthe mean function value and variance (using population variance, ddof=0).\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Design points, shape (n_samples, n_features). | _required_ |\n| y | [ndarray](`ndarray`) | Function values, shape (n_samples,). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| tuple | [Tuple](`typing.Tuple`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`)\\] | A tuple containing: * X_agg (ndarray): Unique design points, shape (n_groups, n_features) * y_mean (ndarray): Mean y values per group, shape (n_groups,) * y_var (ndarray): Variance of y values per group, shape (n_groups,) |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#86f98ff9 .cell execution_count=10}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n repeats_initial=2)\nX = np.array([[1, 2], [3, 4], [1, 2]])\ny = np.array([1, 2, 3])\nX_agg, y_mean, y_var = opt.aggregate_mean_var(X, y)\nprint(X_agg.shape)\nprint(y_mean)\nprint(y_var)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(2, 2)\n[2. 2.]\n[1. 0.]\n```\n:::\n:::\n\n\n### apply_ocba { #spotoptim.SpotOptim.SpotOptim.apply_ocba }\n\n```python\nSpotOptim.SpotOptim.apply_ocba()\n```\n\nApply Optimal Computing Budget Allocation for noisy functions.\n\n### apply_penalty_NA { #spotoptim.SpotOptim.SpotOptim.apply_penalty_NA }\n\n```python\nSpotOptim.SpotOptim.apply_penalty_NA(\n y,\n y_history=None,\n penalty_value=None,\n sd=0.1,\n)\n```\n\nReplace NaN and infinite values with penalty plus random noise.\nUsed in the optimize() method after function evaluations.\nThis method follows the approach from spotpython.utils.repair.apply_penalty_NA,\nreplacing NaN/inf values with a penalty value plus random noise to avoid\nidentical penalty values.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|---------------|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|\n| y | [ndarray](`ndarray`) | Array of objective function values to be repaired. | _required_ |\n| y_history | [ndarray](`ndarray`) | Historical objective function values used for computing penalty statistics. If None, uses y itself. Default is None. | `None` |\n| penalty_value | [float](`float`) | Value to replace NaN/inf with. If None, computes penalty as: max(finite_y_history) + 3 * std(finite_y_history). If all values are NaN/inf or only one finite value exists, falls back to self.penalty_val. Default is None. | `None` |\n| sd | [float](`float`) | Standard deviation for normal distributed random noise added to penalty. Default is 0.1. | `0.1` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Array with NaN/inf replaced by penalty_value + random noise (normal distributed with mean 0 and standard deviation sd). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#e21f4652 .cell execution_count=11}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1), bounds=[(-5, 5)])\ny_hist = np.array([1.0, 2.0, 3.0, 5.0])\ny_new = np.array([4.0, np.nan, np.inf])\ny_clean = opt.apply_penalty_NA(y_new, y_history=y_hist)\nprint(f\"np.all(np.isfinite(y_clean)): {np.all(np.isfinite(y_clean))}\")\nprint(f\"y_clean: {y_clean}\")\n# NaN/inf replaced with worst value from history + 3*std + noise\nprint(f\"y_clean[1] > 5.0: {y_clean[1] > 5.0}\") # Should be larger than max finite value in history\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nnp.all(np.isfinite(y_clean)): True\ny_clean: [ 4. 10.16504055 10.23805534]\ny_clean[1] > 5.0: True\n```\n:::\n:::\n\n\n### check_size_initial_design { #spotoptim.SpotOptim.SpotOptim.check_size_initial_design }\n\n```python\nSpotOptim.SpotOptim.check_size_initial_design(y0, n_evaluated)\n```\n\nValidate that initial design has sufficient points for surrogate fitting.\n\nChecks if the number of valid initial design points meets the minimum\nrequirement for fitting a surrogate model. The minimum required is the\nsmaller of:\n * (a) typical minimum for surrogate fitting (3 for multi-dimensional, 2 for 1D), or\n * (b) what the user requested (`n_initial`).\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-------------|----------------------|-------------------------------------------------------------------------------|------------|\n| y0 | [ndarray](`ndarray`) | Function values at initial design points (after filtering), shape (n_valid,). | _required_ |\n| n_evaluated | [int](`int`) | Original number of points evaluated before filtering. | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|------------------------------------------------------------------|\n| | [ValueError](`ValueError`) | If the number of valid points is less than the minimum required. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#5edda3b7 .cell execution_count=12}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=10\n)\n# Sufficient points - no error\ny0 = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\nopt.check_size_initial_design(y0, n_evaluated=10)\n\n# Insufficient points - raises ValueError\ny0_small = np.array([1.0])\ntry:\n opt.check_size_initial_design(y0_small, n_evaluated=10)\nexcept ValueError as e:\n print(f\"Error: {e}\")\n\n# With verbose output\nopt_verbose = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=10,\n verbose=True\n)\ny0_reduced = np.array([1.0, 2.0, 3.0]) # Less than n_initial but valid\nopt_verbose.check_size_initial_design(y0_reduced, n_evaluated=10)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nError: Insufficient valid initial design points: only 1 finite value(s) out of 10 evaluated. Need at least 3 points to fit surrogate model. Please check your objective function or increase n_initial.\nTensorBoard logging disabled\nNote: Initial design size (3) is smaller than requested (10) due to NaN/inf values\n```\n:::\n:::\n\n\n### curate_initial_design { #spotoptim.SpotOptim.SpotOptim.curate_initial_design }\n\n```python\nSpotOptim.SpotOptim.curate_initial_design(X0)\n```\n\nRemove duplicates and ensure sufficient unique points in initial design.\n\nThis method handles deduplication that can occur after rounding integer/factor\nvariables. If duplicates are found, it generates additional points to reach\nthe target n_initial unique points. Also handles repeating points when\nrepeats_initial > 1.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-------------------------------------------------------------------------|------------|\n| X0 | [ndarray](`ndarray`) | Initial design points in internal scale, shape (n_samples, n_features). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Curated initial design with duplicates removed and repeated if necessary, shape (n_unique * repeats_initial, n_features). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#6550adaf .cell execution_count=13}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=10,\n var_type=['int', 'int'] # Integer variables may cause duplicates\n)\nX0 = opt.get_initial_design()\nX0_curated = opt.curate_initial_design(X0)\nX0_curated.shape[0] == 10 # Should have n_initial unique points\n```\n\n::: {.cell-output .cell-output-display execution_count=13}\n```\nTrue\n```\n:::\n:::\n\n\n::: {#8af0b755 .cell execution_count=14}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n# With repeats\nopt_repeat = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n repeats_initial=3\n)\nX0 = opt_repeat.get_initial_design()\nX0_curated = opt_repeat.curate_initial_design(X0)\nX0_curated.shape[0] == 15 # 5 unique points * 3 repeats\n```\n\n::: {.cell-output .cell-output-display execution_count=14}\n```\nTrue\n```\n:::\n:::\n\n\n### detect_var_type { #spotoptim.SpotOptim.SpotOptim.detect_var_type }\n\n```python\nSpotOptim.SpotOptim.detect_var_type()\n```\n\nAuto-detect variable types based on factor mappings.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|----------------|-------------------------------------------------------------------------------------------------------------------------------------------|\n| list | [list](`list`) | List of variable types ('factor' or 'float') for each dimension. Dimensions with factor mappings are assigned 'factor', others 'float'. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#89a7b989 .cell execution_count=15}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\n\n# Define a simple objective mapping names to values for demonstration\ndef objective(X):\n # X has shape (n_samples, n_dimensions)\n return X[:, 0] + X[:, 1]\n\n# The first dimension has factor levels ('red', 'green', 'blue')\n# The second dimension is continuous bounds (0, 10)\nspot = SpotOptim(fun=objective, bounds=[('red', 'green', 'blue'), (0, 10)])\nprint(spot.detect_var_type())\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n['factor', 'float']\n```\n:::\n:::\n\n\n### determine_termination { #spotoptim.SpotOptim.SpotOptim.determine_termination }\n\n```python\nSpotOptim.SpotOptim.determine_termination(timeout_start)\n```\n\nDetermine termination reason for optimization.\nChecks the termination conditions and returns an appropriate message\nindicating why the optimization stopped. Three possible termination\nconditions are checked in order of priority:\n 1. Maximum number of evaluations reached\n 2. Maximum time limit exceeded\n 3. Successful completion (neither limit reached)\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|---------------|------------------|------------------------------------------------|------------|\n| timeout_start | [float](`float`) | Start time of optimization (from time.time()). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------------|--------------------------------------------|\n| str | [str](`str`) | Message describing the termination reason. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#100ef239 .cell execution_count=16}\n``` {.python .cell-code}\nimport numpy as np\nimport time\nfrom spotoptim import SpotOptim\nopt = SpotOptim(\n fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n max_time=10.0\n)\n# Case 1: Maximum evaluations reached\nopt.y_ = np.zeros(20) # Simulate 20 evaluations\nstart_time = time.time()\nmsg = opt.determine_termination(start_time)\nprint(msg)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nOptimization terminated: maximum evaluations (10) reached\n```\n:::\n:::\n\n\n::: {#270d9cd2 .cell execution_count=17}\n``` {.python .cell-code}\n# Case 2: Time limit exceeded\nimport numpy as np\nimport time\nfrom spotoptim import SpotOptim\nopt.y_ = np.zeros(10) # Only 10 evaluations\nstart_time = time.time() - 700 # Simulate 11.67 minutes elapsed\nmsg = opt.determine_termination(start_time)\nprint(msg)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nOptimization terminated: maximum evaluations (10) reached\n```\n:::\n:::\n\n\n::: {#87a7942d .cell execution_count=18}\n``` {.python .cell-code}\n# Case 3: Successful completion\nimport numpy as np\nimport time\nfrom spotoptim import SpotOptim\nopt.y_ = np.zeros(10) # Under max_iter\nstart_time = time.time() # Just started\nmsg = opt.determine_termination(start_time)\nprint(msg)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nOptimization terminated: maximum evaluations (10) reached\n```\n:::\n:::\n\n\n### evaluate_function { #spotoptim.SpotOptim.SpotOptim.evaluate_function }\n\n```python\nSpotOptim.SpotOptim.evaluate_function(X)\n```\n\nEvaluate objective function at points X.\nUsed in the optimize() method to evaluate the objective function.\n\nInput Space: `X` is expected in Transformed and Mapped Space (Internal scale, Reduced dimensions).\nProcess as follows:\n 1. Expands `X` to Transformed Space (Full dimensions) if dimension reduction is active.\n 2. Inverse transforms `X` to Natural Space (Original scale).\n 3. Evaluates the user function with points in Natural Space.\n\nIf dimension reduction is active, expands `X` to full dimensions before evaluation.\nSupports both single-objective and multi-objective functions. For multi-objective\nfunctions, converts to single-objective using `mo2so` method.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|--------------------------------------------------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Points to evaluate in Transformed and Mapped Space, shape (n_samples, n_reduced_features). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|--------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Function values, shape (n_samples,). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#faec4675 .cell execution_count=19}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n# Single-objective function\nopt_so = SpotOptim(\n fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5\n)\nX = np.array([[1.0, 2.0], [3.0, 4.0]])\ny = opt_so.evaluate_function(X)\nprint(f\"Single-objective output: {y}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSingle-objective output: [ 5. 25.]\n```\n:::\n:::\n\n\n::: {#4ee0ce20 .cell execution_count=20}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n# Multi-objective function (default: use first objective)\nopt_mo = SpotOptim(\n fun=lambda X: np.column_stack([\n np.sum(X**2, axis=1),\n np.sum((X-1)**2, axis=1)\n ]),\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5\n)\ny_mo = opt_mo.evaluate_function(X)\nprint(f\"Multi-objective output (first obj): {y_mo}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nMulti-objective output (first obj): [ 5. 25.]\n```\n:::\n:::\n\n\n### execute_optimization_run { #spotoptim.SpotOptim.SpotOptim.execute_optimization_run }\n\n```python\nSpotOptim.SpotOptim.execute_optimization_run(\n timeout_start,\n X0=None,\n y0_known=None,\n max_iter_override=None,\n shared_best_y=None,\n shared_lock=None,\n)\n```\n\nDispatcher for optimization run (Sequential vs Steady-State Parallel).\nDepending on n_jobs, calls optimize_steady_state (n_jobs > 1) or optimize_sequential_run (n_jobs == 1).\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-------------------|---------------------------------------------------------------------------|------------------------------------------------------------------------|------------|\n| timeout_start | [float](`float`) | Start time for timeout. | _required_ |\n| X0 | [Optional](`typing.Optional`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`)\\] | Initial design points in Natural Space, shape (n_initial, n_features). | `None` |\n| y0_known | [Optional](`typing.Optional`)\\[[float](`float`)\\] | Known best value for initial design. | `None` |\n| max_iter_override | [Optional](`typing.Optional`)\\[[int](`int`)\\] | Override for maximum number of iterations. | `None` |\n| shared_best_y | [Optional](`typing.Optional`)\\[[float](`float`)\\] | Shared best value for parallel runs. | `None` |\n| shared_lock | [Optional](`typing.Optional`)\\[[Lock](`Lock`)\\] | Shared lock for parallel runs. | `None` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|\n| | [Tuple](`typing.Tuple`)\\[[str](`str`), [OptimizeResult](`scipy.optimize.OptimizeResult`)\\] | Tuple[str, OptimizeResult]: Tuple containing status and optimization result. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#efb689d7 .cell execution_count=21}\n``` {.python .cell-code}\nimport time\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n max_iter=10,\n seed=0,\n n_jobs=1, # Use sequential optimization for deterministic output\n verbose=True\n)\nstatus, result = opt.execute_optimization_run(timeout_start=time.time())\nprint(status)\nprint(result.message.splitlines()[0])\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 8.463203\nIter 1 | Best: 8.463203 | Curr: 18.224245 | Rate: 0.00 | Evals: 60.0%\nIter 2 | Best: 8.412459 | Rate: 0.50 | Evals: 70.0%\nIter 3 | Best: 5.623369 | Rate: 0.67 | Evals: 80.0%\nIter 4 | Best: 2.902974 | Rate: 0.75 | Evals: 90.0%\nIter 5 | Best: 0.022050 | Rate: 0.80 | Evals: 100.0%\nFINISHED\nOptimization terminated: maximum evaluations (10) reached\n```\n:::\n:::\n\n\n### fit_scheduler { #spotoptim.SpotOptim.SpotOptim.fit_scheduler }\n\n```python\nSpotOptim.SpotOptim.fit_scheduler()\n```\n\nFit surrogate model using appropriate data based on noise handling.\nThis method selects the appropriate training data for surrogate fitting:\n * For noisy functions (repeats_surrogate > 1): Uses mean_X and mean_y (aggregated values)\n * For deterministic functions: Uses X_ and y_ (all evaluated points)\nThe data is transformed to internal scale before fitting the surrogate.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n```python\n>>> import numpy as np\n>>> from spotoptim import SpotOptim\n>>> from sklearn.gaussian_process import GaussianProcessRegressor\n>>> # Deterministic function\n>>> def sphere(X):\n... X = np.atleast_2d(X)\n... return np.sum(X**2, axis=1)\n>>> opt = SpotOptim(\n... fun=sphere,\n... bounds=[(-5, 5), (-5, 5)],\n... surrogate=GaussianProcessRegressor(),\n... n_initial=5\n... )\n>>> # Simulate optimization state\n>>> opt.X_ = np.array([[1, 2], [0, 0], [2, 1]])\n>>> opt.y_ = np.array([5.0, 0.0, 5.0])\n>>> opt.fit_scheduler()\n>>> # Surrogate fitted with X_ and y_\n>>>\n>>> # Noisy function\n>>> def sphere(X):\n... X = np.atleast_2d(X)\n... return np.sum(X**2, axis=1)\n>>> opt_noise = SpotOptim(\n... fun=sphere,\n... bounds=[(-5, 5), (-5, 5)],\n... surrogate=GaussianProcessRegressor(),\n... n_initial=5,\n... repeats_initial=3,\n... )\n>>> # Simulate noisy optimization state\n>>> opt_noise.mean_X = np.array([[1, 2], [0, 0]])\n>>> opt_noise.mean_y = np.array([5.0, 0.0])\n>>> opt_noise.fit_scheduler()\n>>> # Surrogate fitted with mean_X and mean_y\n```\n\n### fit_select_best_cluster { #spotoptim.SpotOptim.SpotOptim.fit_select_best_cluster }\n\n```python\nSpotOptim.SpotOptim.fit_select_best_cluster(X, y, k)\n```\n\nSelects all points from the cluster with the smallest mean y value.\nThis method performs K-means clustering and selects all points from the\ncluster whose center corresponds to the best (smallest) mean objective\nfunction value.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Design points, shape (n_samples, n_features). | _required_ |\n| y | [ndarray](`ndarray`) | Function values at X, shape (n_samples,). | _required_ |\n| k | [int](`int`) | Number of clusters. | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|---------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| tuple | [Tuple](`typing.Tuple`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`)\\] | A tuple containing: * selected_X (ndarray): Selected design points from best cluster, shape (m, n_features). * selected_y (ndarray): Function values at selected points, shape (m,). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#f0e74a9f .cell execution_count=22}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_surrogate_points=5,\n selection_method='best')\nX = np.random.rand(100, 2)\ny = np.random.rand(100)\nX_sel, y_sel = opt.fit_select_best_cluster(X, y, 5)\nprint(f\"X_sel.shape: {X_sel.shape}\")\nprint(f\"y_sel.shape: {y_sel.shape}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nX_sel.shape: (25, 2)\ny_sel.shape: (25,)\n```\n:::\n:::\n\n\n### fit_select_distant_points { #spotoptim.SpotOptim.SpotOptim.fit_select_distant_points }\n\n```python\nSpotOptim.SpotOptim.fit_select_distant_points(X, y, k)\n```\n\nSelects k points that are distant from each other using K-means clustering.\nThis method performs K-means clustering to find k clusters, then selects\nthe point closest to each cluster center. This ensures a space-filling\nsubset of points for surrogate model training.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Design points, shape (n_samples, n_features). | _required_ |\n| y | [ndarray](`ndarray`) | Function values at X, shape (n_samples,). | _required_ |\n| k | [int](`int`) | Number of points to select. | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|---------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| tuple | [Tuple](`typing.Tuple`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`)\\] | A tuple containing: * selected_X (ndarray): Selected design points, shape (k, n_features). * selected_y (ndarray): Function values at selected points, shape (k,). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#55a2d428 .cell execution_count=23}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_surrogate_points=5)\nX = np.random.rand(100, 2)\ny = np.random.rand(100)\nX_sel, y_sel = opt.fit_select_distant_points(X, y, 5)\nprint(X_sel.shape)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(5, 2)\n```\n:::\n:::\n\n\n### fit_selection_dispatcher { #spotoptim.SpotOptim.SpotOptim.fit_selection_dispatcher }\n\n```python\nSpotOptim.SpotOptim.fit_selection_dispatcher(X, y)\n```\n\nDispatcher for selection methods.\nDepending on the value of `self.selection_method`, this method calls\nthe appropriate selection function to choose a subset of points for\nsurrogate model training when the total number of points exceeds\n`self.max_surrogate_points`.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Design points, shape (n_samples, n_features). | _required_ |\n| y | [ndarray](`ndarray`) | Function values at X, shape (n_samples,). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|---------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------|\n| tuple | [Tuple](`typing.Tuple`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`)\\] | A tuple containing: * selected_X (ndarray): Selected design points. * selected_y (ndarray): Function values at selected points. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#51ab7947 .cell execution_count=24}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_surrogate_points=5)\nX = np.random.rand(100, 2)\ny = np.random.rand(100)\nX_sel, y_sel = opt.fit_selection_dispatcher(X, y)\nprint(X_sel.shape[0] <= 5)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTrue\n```\n:::\n:::\n\n\n### fit_surrogate { #spotoptim.SpotOptim.SpotOptim.fit_surrogate }\n\n```python\nSpotOptim.SpotOptim.fit_surrogate(X, y)\n```\n\nFit surrogate model to data.\nUsed by fit_scheduler() to fit the surrogate model.\nIf the number of points exceeds `self.max_surrogate_points`,\na subset of points is selected using the selection dispatcher.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Design points, shape (n_samples, n_features). | _required_ |\n| y | [ndarray](`ndarray`) | Function values at X, shape (n_samples,). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n```python\n>>> import numpy as np\n>>> from spotoptim import SpotOptim\n>>> from sklearn.gaussian_process import GaussianProcessRegressor\n>>> def sphere(X):\n... X = np.atleast_2d(X)\n... return np.sum(X**2, axis=1)\n>>> opt = SpotOptim(fun=sphere,\n... bounds=[(-5, 5), (-5, 5)],\n... max_surrogate_points=10,\n... surrogate=GaussianProcessRegressor())\n>>> X = np.random.rand(50, 2)\n>>> y = np.random.rand(50)\n>>> opt.fit_surrogate(X, y)\n>>> # Surrogate is now fitted\n```\n\n### gen_design_table { #spotoptim.SpotOptim.SpotOptim.gen_design_table }\n\n```python\nSpotOptim.SpotOptim.gen_design_table(precision=4, tablefmt='github')\n```\n\nGenerate a table of the design or results.\nIf optimization has been run (results available), returns the results table.\nOtherwise, returns the design table (search space configuration).\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------|--------------|-----------------------------------------------------------|------------|\n| tablefmt | [str](`str`) | Table format. Defaults to 'github'. | `'github'` |\n| precision | [int](`int`) | Number of decimal places for float values. Defaults to 4. | `4` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------------|-------------------------|\n| str | [str](`str`) | Formatted table string. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#06d0f82e .cell execution_count=25}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-10, 10), (0, 1)],\n var_name=[\"x1\", \"x2\", \"x3\"],\n var_type=[\"float\", \"int\", \"float\"],\n max_iter=10,\n n_initial=5\n)\ntable = opt.gen_design_table()\nprint(table)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n| name | type | lower | upper | default | transform |\n|--------|--------|---------|---------|-----------|-------------|\n| x1 | float | -5 | 5 | 0 | - |\n| x2 | int | -10 | 10 | 0 | - |\n| x3 | float | 0 | 1 | 0.5 | - |\n```\n:::\n:::\n\n\n### generate_initial_design { #spotoptim.SpotOptim.SpotOptim.generate_initial_design }\n\n```python\nSpotOptim.SpotOptim.generate_initial_design()\n```\n\nGenerate initial space-filling design using Latin Hypercube Sampling.\nUsed in the optimize() method to create the initial set of design points.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|-------------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Initial design points, shape (n_initial, n_features). Points are in the intervals defined by `self.bounds`. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#0f23c358 .cell execution_count=26}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=3,\n var_type=['float', 'int'],\n var_trans=['log10', None])\nX0 = opt.generate_initial_design()\nprint(X0.shape)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(3, 2)\n```\n:::\n:::\n\n\n### get_best_hyperparameters { #spotoptim.SpotOptim.SpotOptim.get_best_hyperparameters }\n\n```python\nSpotOptim.SpotOptim.get_best_hyperparameters(as_dict=True)\n```\n\nGet the best hyperparameter configuration found during optimization.\nIf noise handling is active (repeats_initial > 1 or OCBA), this returns the parameter\nconfiguration associated with the best *mean* objective value. Otherwise, it returns\nthe configuration associated with the absolute best observed value.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|---------|----------------|---------------------------------------------------------------------------------------------------------------------------------|-----------|\n| as_dict | [bool](`bool`) | If True, returns a dictionary mapping parameter names to their values. If False, returns the raw numpy array. Defaults to True. | `True` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|---------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|\n| | [Union](`typing.Union`)\\[[Dict](`typing.Dict`)\\[[str](`str`), [Any](`typing.Any`)\\], [np](`numpy`).[ndarray](`numpy.ndarray`), None\\] | Union[Dict[str, Any], np.ndarray, None]: The best hyperparameter configuration. Returns None if optimization hasn't started (no data). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#c39cbbae .cell execution_count=27}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere,\n bounds=[(-5, 5), (0, 10)],\n n_initial=5,\n var_name=[\"x\", \"y\"],\n verbose=True)\nopt.optimize()\nbest_params = opt.get_best_hyperparameters()\nprint(best_params['x']) # Should be close to 0\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 10.206165\nIter 1 | Best: 10.206165 | Curr: 67.752773 | Rate: 0.00 | Evals: 30.0%\nIter 2 | Best: 0.064127 | Rate: 0.50 | Evals: 35.0%\nIter 3 | Best: 0.064127 | Curr: 0.084231 | Rate: 0.33 | Evals: 40.0%\nIter 4 | Best: 0.010863 | Rate: 0.50 | Evals: 45.0%\nIter 5 | Best: 0.000000 | Rate: 0.60 | Evals: 50.0%\nIter 6 | Best: 0.000000 | Curr: 0.000000 | Rate: 0.50 | Evals: 55.0%\nIter 7 | Best: 0.000000 | Rate: 0.57 | Evals: 60.0%\nIter 8 | Best: 0.000000 | Curr: 0.000000 | Rate: 0.50 | Evals: 65.0%\nIter 9 | Best: 0.000000 | Curr: 0.000000 | Rate: 0.44 | Evals: 70.0%\nIter 10 | Best: 0.000000 | Curr: 0.000000 | Rate: 0.40 | Evals: 75.0%\nIter 11 | Best: 0.000000 | Curr: 0.000000 | Rate: 0.36 | Evals: 80.0%\nIter 12 | Best: 0.000000 | Curr: 0.000000 | Rate: 0.33 | Evals: 85.0%\nIter 13 | Best: 0.000000 | Curr: 0.000000 | Rate: 0.31 | Evals: 90.0%\nIter 14 | Best: 0.000000 | Curr: 0.000000 | Rate: 0.29 | Evals: 95.0%\nIter 15 | Best: 0.000000 | Curr: 0.000000 | Rate: 0.27 | Evals: 100.0%\n-9.823546329330167e-05\n```\n:::\n:::\n\n\n### get_best_xy_initial_design { #spotoptim.SpotOptim.SpotOptim.get_best_xy_initial_design }\n\n```python\nSpotOptim.SpotOptim.get_best_xy_initial_design()\n```\n\nDetermine and store the best point from initial design.\nFinds the best (minimum) function value in the initial design,\nstores the corresponding point and value in instance attributes,\nand optionally prints the results if verbose mode is enabled.\nFor noisy functions, also reports the mean best value.\n\n#### Note {.doc-section .doc-section-note}\n\nThis method assumes self.X_ and self.y_ have been initialized\nwith the initial design evaluations.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#d5f13506 .cell execution_count=28}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n verbose=True\n)\n# Simulate initial design (normally done in optimize())\nopt.X_ = np.array([[1, 2], [0, 0], [2, 1]])\nopt.y_ = np.array([5.0, 0.0, 5.0])\nopt.get_best_xy_initial_design()\nprint(f\"Best x: {opt.best_x_}\")\nprint(f\"Best y: {opt.best_y_}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 0.000000\nBest x: [0 0]\nBest y: 0.0\n```\n:::\n:::\n\n\n::: {#b1576b25 .cell execution_count=29}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import noisy_sphere\n# With noisy function\nopt_noise = SpotOptim(\n fun=noisy_sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n repeats_surrogate=2,\n verbose=True\n)\nopt_noise.X_ = np.array([[1, 2], [0, 0], [2, 1]])\nopt_noise.y_ = np.array([5.0, 0.0, 5.0])\nopt_noise.min_mean_y = 0.5 # Simulated mean best\nopt_noise.get_best_xy_initial_design()\nprint(f\"Best x: {opt_noise.best_x_}\")\nprint(f\"Best y: {opt_noise.best_y_}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 0.000000, mean best: f(x) = 0.500000\nBest x: [0 0]\nBest y: 0.0\n```\n:::\n:::\n\n\n### get_design_table { #spotoptim.SpotOptim.SpotOptim.get_design_table }\n\n```python\nSpotOptim.SpotOptim.get_design_table(tablefmt='github', precision=4)\n```\n\nGet a table string showing the search space design before optimization.\nThis method generates a table displaying the variable names, types, bounds,\nand defaults without requiring an optimization run. Useful for inspecting\nand documenting the search space configuration.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------|--------------|-----------------------------------------------------------|------------|\n| tablefmt | [str](`str`) | Table format for tabulate library. Defaults to 'github'. | `'github'` |\n| precision | [int](`int`) | Number of decimal places for float values. Defaults to 4. | `4` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------------|-------------------------|\n| str | [str](`str`) | Formatted table string. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#be9b6621 .cell execution_count=30}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-10, 10), (0, 1)],\n var_name=[\"x1\", \"x2\", \"x3\"],\n var_type=[\"float\", \"int\", \"float\"],\n max_iter=10,\n n_initial=5\n)\ntable = opt.get_design_table()\nprint(table)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n| name | type | lower | upper | default | transform |\n|--------|--------|---------|---------|-----------|-------------|\n| x1 | float | -5 | 5 | 0 | - |\n| x2 | int | -10 | 10 | 0 | - |\n| x3 | float | 0 | 1 | 0.5 | - |\n```\n:::\n:::\n\n\n### get_experiment_filename { #spotoptim.SpotOptim.SpotOptim.get_experiment_filename }\n\n```python\nSpotOptim.SpotOptim.get_experiment_filename(prefix)\n```\n\nGenerate experiment filename with '_exp.pkl' suffix.\n\n### get_importance { #spotoptim.SpotOptim.SpotOptim.get_importance }\n\n```python\nSpotOptim.SpotOptim.get_importance()\n```\n\nCalculate variable importance scores.\nImportance is computed as the normalized sensitivity of each parameter\nbased on the variation in objective values across the evaluated points.\nHigher scores indicate parameters that have more influence on the objective.\nThe importance is calculated as:\n 1. For each dimension, compute the correlation between parameter values\n and objective values\n 2. Normalize to percentage scale (0-100)\n 3. Higher values indicate more important parameters\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|-------------------------------------------|------------------------------------------------------------------|\n| | [List](`typing.List`)\\[[float](`float`)\\] | List[float]: Importance scores for each dimension (0-100 scale). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#6c9be2c6 .cell execution_count=31}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef test_func(X):\n # x0 has strong effect, x1 has weak effect\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=10,\n n_initial=5,\n seed=42\n)\nresult = opt.optimize()\nimportance = opt.get_importance()\nprint(f\"x0 importance: {importance[0]:.2f}\")\nprint(f\"x1 importance: {importance[1]:.2f}\")\n\n# Use table to display importance\ntable = opt.get_results_table(show_importance=True)\nprint(table)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nx0 importance: 73.24\nx1 importance: 26.76\n| name | type | default | lower | upper | tuned | transform | importance | stars |\n|--------|--------|-----------|---------|---------|---------|-------------|--------------|---------|\n| x0 | float | 0 | -5 | 5 | 0.1701 | - | 73.24 | * |\n| x1 | float | 0 | -5 | 5 | 1.9552 | - | 26.76 | . |\n\nInterpretation: ***: >99%, **: >75%, *: >50%, .: >10%\n```\n:::\n:::\n\n\n### get_initial_design { #spotoptim.SpotOptim.SpotOptim.get_initial_design }\n\n```python\nSpotOptim.SpotOptim.get_initial_design(X0=None)\n```\n\nGenerate or process initial design points. Ensures that design points are in\ninternal (transformed and reduced) scale.\nCalls `generate_initial_design()` if `X0` is None, otherwise processes user-provided `X0`.\nHandles three scenarios:\n * `X0` is None: Generate space-filling design using LHS\n * `X0` is None but starting point(s) `x0` is provided: Generate LHS and include `x0` as first point(s)\n * `X0` is provided: Transform and prepare user-provided initial design\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|-----------|\n| X0 | [ndarray](`ndarray`) | User-provided initial design points in original scale, shape (n_initial, n_features). If None, generates space-filling design. Defaults to None. | `None` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|-----------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Initial design points in internal (transformed and reduced) scale, shape (n_initial, n_features_reduced). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#19217ecd .cell execution_count=32}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nfrom spotoptim.plot.visualization import plot_design_points\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=10\n)\n# Generate default LHS design\nX0 = opt.get_initial_design()\nprint(X0.shape)\nplot_design_points(X0)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(10, 2)\n```\n:::\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim.SpotOptim_files/figure-html/cell-33-output-2.png){width=565 height=469}\n:::\n\n::: {.cell-output .cell-output-display execution_count=32}\n![](SpotOptim.SpotOptim_files/figure-html/cell-33-output-3.png){width=565 height=469}\n:::\n:::\n\n\n::: {#784fb331 .cell execution_count=33}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nfrom spotoptim.plot.visualization import plot_design_points\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=10,\n x0=np.array([0, 0]) # Starting point to include in initial design\n)\nX0 = opt.get_initial_design()\nprint(X0.shape)\nplot_design_points(X0)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(10, 2)\n```\n:::\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim.SpotOptim_files/figure-html/cell-34-output-2.png){width=565 height=469}\n:::\n\n::: {.cell-output .cell-output-display execution_count=33}\n![](SpotOptim.SpotOptim_files/figure-html/cell-34-output-3.png){width=565 height=469}\n:::\n:::\n\n\n### get_ocba { #spotoptim.SpotOptim.SpotOptim.get_ocba }\n\n```python\nSpotOptim.SpotOptim.get_ocba(means, vars, delta, verbose=False)\n```\n\nOptimal Computing Budget Allocation (OCBA).\n\n### get_ocba_X { #spotoptim.SpotOptim.SpotOptim.get_ocba_X }\n\n```python\nSpotOptim.SpotOptim.get_ocba_X(X, means, vars, delta, verbose=False)\n```\n\nCalculate OCBA allocation and repeat input array X.\n\n### get_pickle_safe_optimizer { #spotoptim.SpotOptim.SpotOptim.get_pickle_safe_optimizer }\n\n```python\nSpotOptim.SpotOptim.get_pickle_safe_optimizer(\n unpickleables='file_io',\n verbosity=0,\n)\n```\n\nCreate a pickle-safe copy of the optimizer.\n\n### get_ranks { #spotoptim.SpotOptim.SpotOptim.get_ranks }\n\n```python\nSpotOptim.SpotOptim.get_ranks(x)\n```\n\nReturns ranks of numbers within input array x.\n\n### get_result_filename { #spotoptim.SpotOptim.SpotOptim.get_result_filename }\n\n```python\nSpotOptim.SpotOptim.get_result_filename(prefix)\n```\n\nGenerate result filename with '_res.pkl' suffix.\n\n### get_results_table { #spotoptim.SpotOptim.SpotOptim.get_results_table }\n\n```python\nSpotOptim.SpotOptim.get_results_table(\n tablefmt='github',\n precision=4,\n show_importance=False,\n)\n```\n\nGet a comprehensive table string of optimization results.\nThis method generates a formatted table of the search space configuration,\nbest values found, and optionally variable importance scores.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------------|----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|\n| tablefmt | [str](`str`) | Table format for tabulate library. Options include: 'github', 'grid', 'simple', 'plain', 'html', 'latex', etc. Defaults to 'github'. | `'github'` |\n| precision | [int](`int`) | Number of decimal places for float values. Defaults to 4. | `4` |\n| show_importance | [bool](`bool`) | Whether to include importance scores. Importance is calculated as the normalized standard deviation of each parameter's effect on the objective. Requires multiple evaluations. Defaults to False. | `False` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------------|------------------------------------------------------|\n| str | [str](`str`) | Formatted table string that can be printed or saved. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#dbc2a4f9 .cell execution_count=34}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\n# Example 1: Basic usage after optimization\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5), (-5, 5)],\n var_name=[\"x1\", \"x2\", \"x3\"],\n var_type=[\"float\", \"float\", \"float\"],\n max_iter=10,\n n_initial=5\n)\nresult = opt.optimize()\ntable = opt.get_results_table()\nprint(table)\ntable = opt.get_results_table(show_importance=True)\nprint(table)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n| name | type | default | lower | upper | tuned | transform |\n|--------|--------|-----------|---------|---------|---------|-------------|\n| x1 | float | 0 | -5 | 5 | -0.2903 | - |\n| x2 | float | 0 | -5 | 5 | -0.3228 | - |\n| x3 | float | 0 | -5 | 5 | 0.1977 | - |\n| name | type | default | lower | upper | tuned | transform | importance | stars |\n|--------|--------|-----------|---------|---------|---------|-------------|--------------|---------|\n| x1 | float | 0 | -5 | 5 | -0.2903 | - | 54.67 | * |\n| x2 | float | 0 | -5 | 5 | -0.3228 | - | 41.15 | . |\n| x3 | float | 0 | -5 | 5 | 0.1977 | - | 4.18 | |\n\nInterpretation: ***: >99%, **: >75%, *: >50%, .: >10%\n```\n:::\n:::\n\n\n### get_shape { #spotoptim.SpotOptim.SpotOptim.get_shape }\n\n```python\nSpotOptim.SpotOptim.get_shape(y)\n```\n\nGet the shape of the objective function output.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------------------------------------|------------|\n| y | [ndarray](`ndarray`) | Objective function output, shape (n_samples,) or (n_samples, n_objectives). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|----------------------------------------------------------------------------------------|----------------------------------------------------------------------------|\n| tuple | [Tuple](`typing.Tuple`)\\[[int](`int`), [Optional](`typing.Optional`)\\[[int](`int`)\\]\\] | (n_samples, n_objectives) where n_objectives is None for single-objective. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#70129b84 .cell execution_count=35}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(\n fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5\n)\ny_single = np.array([1.0, 2.0, 3.0])\nn, m = opt.get_shape(y_single)\nprint(f\"n={n}, m={m}\")\ny_multi = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])\nn, m = opt.get_shape(y_multi)\nprint(f\"n={n}, m={m}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nn=3, m=None\nn=3, m=2\n```\n:::\n:::\n\n\n### get_stars { #spotoptim.SpotOptim.SpotOptim.get_stars }\n\n```python\nSpotOptim.SpotOptim.get_stars(input_list)\n```\n\nConverts a list of values to a list of stars.\nUsed to visualize the importance of a variable.\nThresholds: >99: ***, >75: **, >50: *, >10: .\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|------------|----------------|--------------------------------------|------------|\n| input_list | [list](`list`) | A list of importance scores (0-100). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|----------------|-------------------------|\n| list | [list](`list`) | A list of star strings. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#f7152d26 .cell execution_count=36}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\n\ndef test_func(X):\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=10,\n n_initial=5,\n seed=42\n)\nopt.optimize()\nopt.get_stars([100, 75, 50, 10, 0])\n```\n\n::: {.cell-output .cell-output-display execution_count=36}\n```\n['***', '*', '.', '', '']\n```\n:::\n:::\n\n\n### get_success_rate { #spotoptim.SpotOptim.SpotOptim.get_success_rate }\n\n```python\nSpotOptim.SpotOptim.get_success_rate()\n```\n\nGet the current success rate of the optimization process.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|------------------|---------------------------|\n| float | [float](`float`) | The current success rate. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#b9bb5467 .cell execution_count=37}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda x: x,\n bounds=[(-5, 5), (-5, 5)])\nprint(opt.get_success_rate())\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n0.0\n```\n:::\n:::\n\n\n### handle_default_var_trans { #spotoptim.SpotOptim.SpotOptim.handle_default_var_trans }\n\n```python\nSpotOptim.SpotOptim.handle_default_var_trans()\n```\n\nHandle default variable transformations. Does not perform any transformations,\nonly sets `var_trans` to a list of `None` values if not specified, or normalizes\ntransformation names by converting `id`, `None`, or `None` to `None`.\nAlso validates that `var_trans` length matches the number of dimensions.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|------------------------------------------|\n| | [ValueError](`ValueError`) | If var_trans length doesn't match n_dim. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#79e86d48 .cell execution_count=38}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\n# Default behavior - all None\nspot = SpotOptim(fun=lambda x: x, bounds=[(0, 10), (0, 10)])\nprint(f\"spot.var_trans (should be [None, None]): {spot.var_trans}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nspot.var_trans (should be [None, None]): [None, None]\n```\n:::\n:::\n\n\n::: {#44bfe2bd .cell execution_count=39}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\n# Normalize transformation names\nspot = SpotOptim(fun=lambda x: x, bounds=[(1, 10), (1, 100)],\n var_trans=['log10', 'id'])\nprint(f\"spot.var_trans (should be ['log10', 'None']): {spot.var_trans}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nspot.var_trans (should be ['log10', 'None']): ['log10', None]\n```\n:::\n:::\n\n\n### init_storage { #spotoptim.SpotOptim.SpotOptim.init_storage }\n\n```python\nSpotOptim.SpotOptim.init_storage(X0, y0)\n```\n\nInitialize storage for optimization.\nSets up the initial data structures needed for optimization tracking:\n * X_: Evaluated design points (in original scale)\n * y_: Function values at evaluated points\n * n_iter_: Iteration counter\nThen updates statistics by calling `update_stats()`.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-------------------------------------------------------------------------|------------|\n| X0 | [ndarray](`ndarray`) | Initial design points in internal scale, shape (n_samples, n_features). | _required_ |\n| y0 | [ndarray](`ndarray`) | Function values at X0, shape (n_samples,). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#9686a31b .cell execution_count=40}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5)\nX0 = np.array([[1, 2], [3, 4], [0, 1]])\ny0 = np.array([5.0, 25.0, 1.0])\nopt.init_storage(X0, y0)\nprint(f\"X_ = {opt.X_}\")\nprint(f\"y_ = {opt.y_}\")\nprint(f\"n_iter_ = {opt.n_iter_}\")\nprint(f\"counter = {opt.counter}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nX_ = [[1 2]\n [3 4]\n [0 1]]\ny_ = [ 5. 25. 1.]\nn_iter_ = 0\ncounter = 0\n```\n:::\n:::\n\n\n### init_surrogate { #spotoptim.SpotOptim.SpotOptim.init_surrogate }\n\n```python\nSpotOptim.SpotOptim.init_surrogate()\n```\n\nInitialize or configure the surrogate model for optimization. Handles three surrogate configurations:\n * List of surrogates: sets up multi-surrogate selection with probability weights and per-surrogate `max_surrogate_points`.\n * None (default): creates a `GaussianProcessRegressor` with a\n `ConstantKernel * Matern(nu=2.5)` kernel, 100 optimizer restarts,\n and `normalize_y=True`.\n * User-provided surrogate: accepted as-is; internal bookkeeping\n attributes (`_max_surrogate_points_list`,\n `_active_max_surrogate_points`) are still initialised.\nAfter this method returns the following attributes are set:\n * `self.surrogate` — the active surrogate model.\n * `self._surrogates_list` — `list | None`.\n * `self._prob_surrogate` — normalised selection probabilities or `None`.\n * `self._max_surrogate_points_list` — per-surrogate point caps or `None`.\n * `self._active_max_surrogate_points` — active cap.\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|---------------------------------------------------------------------------------|\n| | [ValueError](`ValueError`) | If the surrogate list is empty. |\n| | [ValueError](`ValueError`) | If 'prob_surrogate' length does not match the surrogate list length. |\n| | [ValueError](`ValueError`) | If 'max_surrogate_points' list length does not match the surrogate list length. |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#d0acbe25 .cell execution_count=41}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n# Default surrogate (GaussianProcessRegressor)\nopt = SpotOptim(\n fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n)\nprint(type(opt.surrogate).__name__)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nGaussianProcessRegressor\n```\n:::\n:::\n\n\n::: {#b75b5c58 .cell execution_count=42}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom sklearn.ensemble import RandomForestRegressor\n# User-provided surrogate\nrf = RandomForestRegressor(n_estimators=50, random_state=42)\nopt = SpotOptim(\n fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n surrogate=rf,\n)\nprint(type(opt.surrogate).__name__)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nRandomForestRegressor\n```\n:::\n:::\n\n\n::: {#a053d4be .cell execution_count=43}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom sklearn.ensemble import RandomForestRegressor\nfrom sklearn.gaussian_process import GaussianProcessRegressor\n# List of surrogates with selection probabilities\nsurrogates = [GaussianProcessRegressor(), RandomForestRegressor()]\nopt = SpotOptim(\n fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n surrogate=surrogates,\n prob_surrogate=[0.7, 0.3],\n)\nprint(opt._prob_surrogate)\nprint([type(s).__name__ for s in opt._surrogates_list])\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[0.7, 0.3]\n['GaussianProcessRegressor', 'RandomForestRegressor']\n```\n:::\n:::\n\n\n### inverse_transform_X { #spotoptim.SpotOptim.SpotOptim.inverse_transform_X }\n\n```python\nSpotOptim.SpotOptim.inverse_transform_X(X)\n```\n\nTransform parameter array from internal to original scale.\nConverts from transformed space (full dimension) to natural space (original).\nDoes NOT handle dimension expansion (un-mapping).\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Array in Transformed Space, shape (n_samples, n_features) | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Array in Natural Space |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#eac8fa91 .cell execution_count=44}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nimport numpy as np\nspot = SpotOptim(fun=sphere, bounds=[(1, 10)], var_trans=['log10'])\nX_trans = np.array([[0], [1], [2]])\nspot.inverse_transform_X(X_trans)\n```\n\n::: {.cell-output .cell-output-display execution_count=44}\n```\narray([[ 1],\n [ 10],\n [100]])\n```\n:::\n:::\n\n\n### inverse_transform_value { #spotoptim.SpotOptim.SpotOptim.inverse_transform_value }\n\n```python\nSpotOptim.SpotOptim.inverse_transform_value(x, trans)\n```\n\nApply inverse transformation to a single float value.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|-----------------------------------------------|----------------------|------------|\n| x | [float](`float`) | Transformed value | _required_ |\n| trans | [Optional](`typing.Optional`)\\[[str](`str`)\\] | Transformation name. | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|------------------|----------------|\n| | [float](`float`) | Original value |\n\n#### Notes {.doc-section .doc-section-notes}\n\nSee also transform_value.\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#1e0d9c36 .cell execution_count=45}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nspot = SpotOptim(fun=sphere, bounds=[(1, 10)])\nspot.inverse_transform_value(10, 'log10')\nspot.inverse_transform_value(100, 'log(x)')\n```\n\n::: {.cell-output .cell-output-display execution_count=45}\n```\nnp.float64(2.6881171418161356e+43)\n```\n:::\n:::\n\n\n### load_experiment { #spotoptim.SpotOptim.SpotOptim.load_experiment }\n\n```python\nSpotOptim.SpotOptim.load_experiment(filename)\n```\n\nLoad experiment configuration from a pickle file.\n\n### load_result { #spotoptim.SpotOptim.SpotOptim.load_result }\n\n```python\nSpotOptim.SpotOptim.load_result(filename)\n```\n\nLoad complete optimization results from a pickle file.\n\n### map_to_factor_values { #spotoptim.SpotOptim.SpotOptim.map_to_factor_values }\n\n```python\nSpotOptim.SpotOptim.map_to_factor_values(X)\n```\n\nMap internal integer factor values back to string labels.\nFor factor variables, converts integer indices back to original string values.\nOther variable types remain unchanged.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-------------------------------------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Design points with integer values for factors, shape (n_samples, n_features). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Design points with factor integers replaced by string labels. Dtype will be object or string if mixed types are present. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#89406a4a .cell execution_count=46}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nimport numpy as np\nspot = SpotOptim(\n fun=sphere,\n bounds=[('red', 'blue'), (0, 10)]\n)\nspot.process_factor_bounds()\nX_int = np.array([[0, 5.0], [1, 8.0]])\nX_str = spot.map_to_factor_values(X_int)\nprint(X_str[0])\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n['red' 5.0]\n```\n:::\n:::\n\n\n### mo2so { #spotoptim.SpotOptim.SpotOptim.mo2so }\n\n```python\nSpotOptim.SpotOptim.mo2so(y_mo)\n```\n\nConvert multi-objective values to single-objective.\nConverts multi-objective values to a single-objective value by applying a user-defined\nfunction from `fun_mo2so`. If no user-defined function is given, the\nvalues in the first objective column are used.\n\nThis method is called after the objective function evaluation. It returns a 1D array\nwith the single-objective values.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|----------------------------------------------------------------------------------------------------------|------------|\n| y_mo | [ndarray](`ndarray`) | If multi-objective, shape (n_samples, n_objectives). If single-objective, shape (n_samples,). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|----------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Single-objective values, shape (n_samples,). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#8a3734eb .cell execution_count=47}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\n# Multi-objective function\ndef mo_fun(X):\n return np.column_stack([\n np.sum(X**2, axis=1),\n np.sum((X-1)**2, axis=1)\n ])\n\n# Example 1: Default behavior (use first objective)\nopt1 = SpotOptim(\n fun=mo_fun,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5\n)\ny_mo = np.array([[1.0, 2.0], [3.0, 4.0]])\ny_so = opt1.mo2so(y_mo)\nprint(f\"Single-objective (default): {y_so}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSingle-objective (default): [1. 3.]\n```\n:::\n:::\n\n\n::: {#f6aa2f08 .cell execution_count=48}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n# Example 2: Custom conversion function (sum of objectives)\ndef custom_mo2so(y_mo):\n return y_mo[:, 0] + y_mo[:, 1]\n\nopt2 = SpotOptim(\n fun=mo_fun,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5,\n fun_mo2so=custom_mo2so\n)\ny_so_custom = opt2.mo2so(y_mo)\nprint(f\"Single-objective (custom): {y_so_custom}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSingle-objective (custom): [3. 7.]\n```\n:::\n:::\n\n\n### modify_bounds_based_on_var_type { #spotoptim.SpotOptim.SpotOptim.modify_bounds_based_on_var_type }\n\n```python\nSpotOptim.SpotOptim.modify_bounds_based_on_var_type()\n```\n\nModify bounds based on variable types.\nAdjusts bounds for each dimension according to its var_type:\n * 'int': Ensures bounds are integers (ceiling for lower, floor for upper)\n * 'factor': Bounds already set to (0, n_levels-1) by process_factor_bounds\n * 'float': Explicitly converts bounds to float\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|--------------------------------------------|\n| | [ValueError](`ValueError`) | If an unsupported var_type is encountered. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#d1b82293 .cell execution_count=49}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nspot = SpotOptim(fun=lambda x: x, bounds=[(0.5, 10.5)], var_type=['int'])\nprint(spot.bounds)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[(1, 10)]\n```\n:::\n:::\n\n\n::: {#c019053e .cell execution_count=50}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nspot = SpotOptim(fun=lambda x: x, bounds=[(0, 10)], var_type=['float'])\nprint(spot.bounds)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[(0.0, 10.0)]\n```\n:::\n:::\n\n\n### optimize { #spotoptim.SpotOptim.SpotOptim.optimize }\n\n```python\nSpotOptim.SpotOptim.optimize(X0=None)\n```\n\nRun the optimization process. The optimization terminates when either the total function evaluations reach\n `max_iter` (including initial design), or the runtime exceeds max_time minutes. Input/Output spaces are\n * Input `X0`: Expected in Natural Space (original scale, physical units).\n * Output `result.x`: Returned in Natural Space.\n * Output `result.X`: Returned in Natural Space.\n * Internal Optimization: Performed in Transformed and Mapped Space.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------------------------------------------------------------------------------------------|-----------|\n| X0 | [ndarray](`ndarray`) | Initial design points in Natural Space, shape (n_initial, n_features). If None, generates space-filling design. Defaults to None. | `None` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|----------------|---------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| OptimizeResult | [OptimizeResult](`scipy.optimize.OptimizeResult`) | Optimization result with fields: * x: best point found in Natural Space * fun: best function value * nfev: number of function evaluations (including initial design) * nit: number of sequential optimization iterations (after initial design) * success: whether optimization succeeded * message: termination message indicating reason for stopping, including statistics (function value, iterations, evaluations) * X: all evaluated points in Natural Space * y: all function values |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#36bd94a0 .cell execution_count=51}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n max_iter=10,\n seed=0,\n x0=np.array([0.1, -0.1]),\n verbose=True\n)\nresult = opt.optimize()\nprint(result.message.splitlines()[0])\nprint(\"Best point:\", result.x)\nprint(\"Best value:\", result.fun)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nStarting point x0 validated and processed successfully.\n Original scale: [ 0.1 -0.1]\n Internal scale: [ 0.1 -0.1]\nTensorBoard logging disabled\nIncluding 1 starting points from x0 in initial design.\nInitial best: f(x) = 0.020000\nIter 1 | Best: 0.020000 | Curr: 14.707944 | Rate: 0.00 | Evals: 60.0%\nOptimizer candidate 1/3 was duplicate/invalid.\nIter 2 | Best: 0.020000 | Curr: 0.020020 | Rate: 0.00 | Evals: 70.0%\nIter 3 | Best: 0.020000 | Curr: 0.322921 | Rate: 0.00 | Evals: 80.0%\nIter 4 | Best: 0.002244 | Rate: 0.25 | Evals: 90.0%\nIter 5 | Best: 0.002179 | Rate: 0.40 | Evals: 100.0%\nOptimization terminated: maximum evaluations (10) reached\nBest point: [0.04474339 0.01330855]\nBest value: 0.002179088112986503\n```\n:::\n:::\n\n\n### optimize_acquisition_func { #spotoptim.SpotOptim.SpotOptim.optimize_acquisition_func }\n\n```python\nSpotOptim.SpotOptim.optimize_acquisition_func()\n```\n\nOptimize the acquisition function to find the next point to evaluate.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | The optimized point(s). If acquisition_fun_return_size == 1, returns 1D array of shape (n_features,). If acquisition_fun_return_size > 1, returns 2D array of shape (N, n_features), where N is min(acquisition_fun_return_size, population_size). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#21fce229 .cell execution_count=52}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n max_iter=10,\n seed=0,\n)\nopt.optimize()\nx_next = opt.suggest_next_infill_point()\nprint(\"Next point to evaluate:\", x_next)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nNext point to evaluate: [[0.14356455 0.03793884]]\n```\n:::\n:::\n\n\n### optimize_sequential_run { #spotoptim.SpotOptim.SpotOptim.optimize_sequential_run }\n\n```python\nSpotOptim.SpotOptim.optimize_sequential_run(\n timeout_start,\n X0=None,\n y0_known=None,\n max_iter_override=None,\n shared_best_y=None,\n shared_lock=None,\n)\n```\n\nPerform a single sequential optimization run.\nCalls _initialize_run, rm_initial_design_NA_values, check_size_initial_design, init_storage, get_best_xy_initial_design, and _run_sequential_loop.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-------------------|---------------------------------------------------------------------------|------------------------------------------------------------------------|------------|\n| timeout_start | [float](`float`) | Start time for timeout. | _required_ |\n| X0 | [Optional](`typing.Optional`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`)\\] | Initial design points in Natural Space, shape (n_initial, n_features). | `None` |\n| y0_known | [Optional](`typing.Optional`)\\[[float](`float`)\\] | Known best value for initial design. | `None` |\n| max_iter_override | [Optional](`typing.Optional`)\\[[int](`int`)\\] | Override for maximum number of iterations. | `None` |\n| shared_best_y | [Optional](`typing.Optional`)\\[[float](`float`)\\] | Shared best value for parallel runs. | `None` |\n| shared_lock | [Optional](`typing.Optional`)\\[[Lock](`Lock`)\\] | Shared lock for parallel runs. | `None` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|\n| | [Tuple](`typing.Tuple`)\\[[str](`str`), [OptimizeResult](`scipy.optimize.OptimizeResult`)\\] | Tuple[str, OptimizeResult]: Tuple containing status and optimization result. |\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|----------------------------------------------------------------------------------------------------------------------------|\n| | [ValueError](`ValueError`) | If the initial design has no valid points after removing NaN/inf values, or if the initial design is too small to proceed. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#7903f489 .cell execution_count=53}\n``` {.python .cell-code}\nimport time\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n max_iter=10,\n seed=0,\n n_jobs=1, # Use sequential optimization for deterministic output\n verbose=True\n )\nstatus, result = opt.optimize_sequential_run(timeout_start=time.time())\nprint(status)\nprint(result.message.splitlines()[0])\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 8.463203\nIter 1 | Best: 8.463203 | Curr: 18.224245 | Rate: 0.00 | Evals: 60.0%\nIter 2 | Best: 8.412459 | Rate: 0.50 | Evals: 70.0%\nIter 3 | Best: 5.623369 | Rate: 0.67 | Evals: 80.0%\nIter 4 | Best: 2.902974 | Rate: 0.75 | Evals: 90.0%\nIter 5 | Best: 0.022050 | Rate: 0.80 | Evals: 100.0%\nFINISHED\nOptimization terminated: maximum evaluations (10) reached\n```\n:::\n:::\n\n\n### optimize_steady_state { #spotoptim.SpotOptim.SpotOptim.optimize_steady_state }\n\n```python\nSpotOptim.SpotOptim.optimize_steady_state(\n timeout_start,\n X0,\n y0_known=None,\n max_iter_override=None,\n)\n```\n\nPerform steady-state asynchronous optimization (n_jobs > 1).\nThis method implements a hybrid steady-state parallelization strategy.\nThe executor types are selected at runtime based on GIL availability:\nStandard GIL build (Python ≤ 3.12 or GIL-enabled 3.13+):\n * ``ProcessPoolExecutor`` (``eval_pool``) — objective function evaluations.\n Process isolation ensures arbitrary callables (lambdas, closures)\n serialized with ``dill`` run safely without touching shared state.\n * ``ThreadPoolExecutor`` (``search_pool``) — surrogate search tasks.\n Threads share the main-process heap; zero ``dill`` overhead.\n A ``threading.Lock`` (``_surrogate_lock``) prevents a surrogate refit\n from racing with an in-flight search thread.\nFree-threaded build (``python3.13t`` / ``--disable-gil``):\n * Both ``eval_pool`` and ``search_pool`` are ``ThreadPoolExecutor``\n instances. Threads achieve true CPU-level parallelism without the GIL.\n The ``dill`` serialization step for eval tasks is eliminated — ``fun``\n is called directly from the shared heap. The ``_surrogate_lock`` is\n still used to serialize surrogate reads and refits.\nPipeline:\n 1. Parallel Initial Design:\n ``n_initial`` points are dispatched to ``eval_pool``. Results are\n collected via ``FIRST_COMPLETED`` until all initial evaluations finish.\n 2. First Surrogate Fit:\n Called on the main thread once all initial evaluations are in.\n No lock is needed here because no search threads are active yet.\n 3. Parallel Search (Thread Pool):\n Up to ``n_jobs`` search tasks are submitted to ``search_pool``.\n Each acquires ``_surrogate_lock`` before calling\n ``suggest_next_infill_point()``, serializing concurrent surrogate reads.\n 4. Steady-State Loop with Batch Dispatch:\n - Search completes → candidate appended to ``pending_cands``.\n - When ``len(pending_cands) >= eval_batch_size`` (or no search tasks\n remain), all pending candidates are stacked into ``X_batch`` and\n dispatched as a single eval call to ``eval_pool``.\n On GIL builds this calls ``remote_batch_eval_wrapper`` (dill);\n on free-threaded builds it calls ``fun`` directly in a thread.\n - Batch eval completes → storage updated for every point, surrogate\n refit once under ``_surrogate_lock``, new search slots filled.\n - ``eval_batch_size=1`` (default) dispatches immediately on each\n search completion, preserving the original one-point behavior.\n - This cycle continues until ``max_iter`` evaluations or ``max_time``\n minutes is reached.\nThe optimization terminates when either:\n- Total function evaluations reach ``max_iter`` (including initial design), OR\n- Runtime exceeds ``max_time`` minutes\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-------------------|---------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|\n| timeout_start | [float](`float`) | Start time for timeout. | _required_ |\n| X0 | [Optional](`typing.Optional`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`)\\] | Initial design points in Natural Space, shape (n_initial, n_features). | _required_ |\n| y0_known | [Optional](`typing.Optional`)\\[[float](`float`)\\] | Known best objective value from a previous run. When provided together with ``self.x0``, the matching point in the initial design is pre-filled with this value and not re-submitted to the worker pool, saving one evaluation per restart (restart injection). | `None` |\n| max_iter_override | [Optional](`typing.Optional`)\\[[int](`int`)\\] | Override for maximum number of iterations. | `None` |\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| | [RuntimeError](`RuntimeError`) | If all initial design evaluations fail, likely due to pickling issues or missing imports in the worker process. The error message provides guidance on how to address this issue. |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|\n| | [Tuple](`typing.Tuple`)\\[[str](`str`), [OptimizeResult](`scipy.optimize.OptimizeResult`)\\] | Tuple[str, OptimizeResult]: Tuple containing status and optimization result. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#3d585c5b .cell execution_count=54}\n``` {.python .cell-code}\nimport time\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n max_iter=10,\n seed=0,\n n_jobs=2,\n)\nstatus, result = opt.optimize_steady_state(timeout_start=time.time(), X0=None)\nprint(status)\nprint(result.message.splitlines()[0])\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nFINISHED\nOptimization finished (Steady State)\n```\n:::\n:::\n\n\n### plot_importance { #spotoptim.SpotOptim.SpotOptim.plot_importance }\n\n```python\nSpotOptim.SpotOptim.plot_importance(threshold=0.0, figsize=(10, 6))\n```\n\nPlot variable importance.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------|------------------|---------------------------------------------------|-----------|\n| threshold | [float](`float`) | Minimum importance percentage to include in plot. | `0.0` |\n| figsize | [tuple](`tuple`) | Figure size. | `(10, 6)` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#cbb699dc .cell execution_count=55}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\n\ndef test_func(X):\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=10,\n n_initial=5,\n seed=42\n)\nopt.optimize()\nopt.plot_importance()\n```\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim.SpotOptim_files/figure-html/cell-56-output-1.png){width=789 height=523}\n:::\n:::\n\n\n### plot_important_hyperparameter_contour { #spotoptim.SpotOptim.SpotOptim.plot_important_hyperparameter_contour }\n\n```python\nSpotOptim.SpotOptim.plot_important_hyperparameter_contour(\n max_imp=3,\n show=True,\n alpha=0.8,\n cmap='jet',\n num=100,\n add_points=True,\n grid_visible=True,\n contour_levels=30,\n figsize=(12, 10),\n)\n```\n\nPlot surrogate contours using spotoptim.plot.visualization.plot_important_hyperparameter_contour.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|----------------|------------------|----------------------------------------------------------|------------|\n| max_imp | [int](`int`) | The maximum number of important hyperparameters to plot. | `3` |\n| show | [bool](`bool`) | Whether to show the plot. | `True` |\n| alpha | [float](`float`) | The alpha value for the plot. | `0.8` |\n| cmap | [str](`str`) | The colormap to use. | `'jet'` |\n| num | [int](`int`) | The number of points to use for the plot. | `100` |\n| add_points | [bool](`bool`) | Whether to add points to the plot. | `True` |\n| grid_visible | [bool](`bool`) | Whether to show the grid. | `True` |\n| contour_levels | [int](`int`) | The number of contour levels to use. | `30` |\n| figsize | [tuple](`tuple`) | The size of the plot. | `(12, 10)` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#ce3b3f37 .cell execution_count=56}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\n\ndef test_func(X):\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\n# 2-D problem: max_imp must not exceed n_dim (2)\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=10,\n n_initial=5,\n seed=42\n)\nopt.optimize()\nopt.plot_important_hyperparameter_contour(max_imp=2)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nPlotting surrogate contours for top 2 most important parameters:\n x0: importance = 73.24% (type: float)\n x1: importance = 26.76% (type: float)\n\nGenerating 1 surrogate plots...\n Plotting x0 vs x1\n```\n:::\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim.SpotOptim_files/figure-html/cell-57-output-2.png){width=1113 height=950}\n:::\n:::\n\n\n### plot_parameter_scatter { #spotoptim.SpotOptim.SpotOptim.plot_parameter_scatter }\n\n```python\nSpotOptim.SpotOptim.plot_parameter_scatter(\n result=None,\n show=True,\n figsize=(12, 10),\n ylabel='Objective Value',\n cmap='viridis_r',\n show_correlation=False,\n log_y=False,\n)\n```\n\nPlot parameter distributions showing relationship between each parameter and objective.\nCreates a grid of scatter plots, one for each parameter dimension, showing how\nthe objective function value varies with each parameter. The best configuration\nis marked with a red star. Parameters with log-scale transformations (var_trans)\nare automatically displayed on a log x-axis.\nOptionally displays Spearman correlation coefficients in plot titles for\nsensitivity analysis. For factor (categorical) variables, correlation is not\ncomputed and they are displayed with discrete positions on the x-axis.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|------------------|---------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|---------------------|\n| result | [OptimizeResult](`scipy.optimize.OptimizeResult`) | Optimization result containing best parameters. If None, uses the best found values from self.best_x_ and self.best_y_. | `None` |\n| show | [bool](`bool`) | Whether to display the plot. Defaults to True. | `True` |\n| figsize | [tuple](`tuple`) | Figure size as (width, height). Defaults to (12, 10). | `(12, 10)` |\n| ylabel | [str](`str`) | Label for y-axis. Defaults to \"Objective Value\". | `'Objective Value'` |\n| cmap | [str](`str`) | Colormap for scatter plot. Defaults to \"viridis_r\". | `'viridis_r'` |\n| show_correlation | [bool](`bool`) | Whether to compute and display Spearman correlation coefficients in plot titles. Requires scipy. Defaults to False. | `False` |\n| log_y | [bool](`bool`) | Whether to use logarithmic scale for y-axis. Defaults to False. | `False` |\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|---------------------------------------|\n| | [ValueError](`ValueError`) | If no optimization data is available. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#67d36ee7 .cell execution_count=57}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef objective(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(\n fun=objective,\n bounds=[(-5, 5), (-5, 5), (-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\", \"x2\", \"x3\"],\n max_iter=10,\n n_initial=5,\n seed=42\n)\nresult = opt.optimize()\n# Plot parameter distributions\nopt.plot_parameter_scatter(result)\n# Plot with custom settings\nopt.plot_parameter_scatter(result, cmap=\"plasma\", ylabel=\"Error\")\n```\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim.SpotOptim_files/figure-html/cell-58-output-1.png){width=1141 height=949}\n:::\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim.SpotOptim_files/figure-html/cell-58-output-2.png){width=1141 height=949}\n:::\n:::\n\n\n### plot_progress { #spotoptim.SpotOptim.SpotOptim.plot_progress }\n\n```python\nSpotOptim.SpotOptim.plot_progress(\n show=True,\n log_y=False,\n figsize=(10, 6),\n ylabel='Objective Value',\n mo=False,\n)\n```\n\nPlot optimization progress using spotoptim.plot.visualization.plot_progress.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|---------|------------------|----------------------------------------------|---------------------|\n| show | [bool](`bool`) | Whether to show the plot. | `True` |\n| log_y | [bool](`bool`) | Whether to use a logarithmic y-axis. | `False` |\n| figsize | [tuple](`tuple`) | The size of the plot. | `(10, 6)` |\n| ylabel | [str](`str`) | The label for the y-axis. | `'Objective Value'` |\n| mo | [bool](`bool`) | Whether the optimization is multi-objective. | `False` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#061e5e5e .cell execution_count=58}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\n\ndef test_func(X):\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=10,\n n_initial=5,\n seed=42\n)\nopt.optimize()\nopt.plot_progress()\n```\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim.SpotOptim_files/figure-html/cell-59-output-1.png){width=949 height=565}\n:::\n:::\n\n\n### plot_surrogate { #spotoptim.SpotOptim.SpotOptim.plot_surrogate }\n\n```python\nSpotOptim.SpotOptim.plot_surrogate(\n i=0,\n j=1,\n show=True,\n alpha=0.8,\n var_name=None,\n cmap='jet',\n num=100,\n vmin=None,\n vmax=None,\n add_points=True,\n grid_visible=True,\n contour_levels=30,\n figsize=(12, 10),\n)\n```\n\nPlot the surrogate model for two dimensions.\nDelegates to spotoptim.plot.visualization.plot_surrogate.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|----------------|------------------------------------------------------------------------|-------------------------------------------|------------|\n| i | [int](`int`) | The index of the first dimension. | `0` |\n| j | [int](`int`) | The index of the second dimension. | `1` |\n| show | [bool](`bool`) | Whether to show the plot. | `True` |\n| alpha | [float](`float`) | The alpha value for the plot. | `0.8` |\n| var_name | [Optional](`typing.Optional`)\\[[List](`typing.List`)\\[[str](`str`)\\]\\] | The names of the variables. | `None` |\n| cmap | [str](`str`) | The colormap to use. | `'jet'` |\n| num | [int](`int`) | The number of points to use for the plot. | `100` |\n| vmin | [Optional](`typing.Optional`)\\[[float](`float`)\\] | The minimum value for the plot. | `None` |\n| vmax | [Optional](`typing.Optional`)\\[[float](`float`)\\] | The maximum value for the plot. | `None` |\n| add_points | [bool](`bool`) | Whether to add points to the plot. | `True` |\n| grid_visible | [bool](`bool`) | Whether to show the grid. | `True` |\n| contour_levels | [int](`int`) | The number of contour levels to use. | `30` |\n| figsize | [tuple](`tuple`) | The size of the plot. | `(12, 10)` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#a569f1dd .cell execution_count=59}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\n\ndef test_func(X):\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=10,\n n_initial=5,\n seed=42\n)\nopt.optimize()\nopt.plot_surrogate()\n```\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim.SpotOptim_files/figure-html/cell-60-output-1.png){width=1113 height=950}\n:::\n:::\n\n\n### print_best { #spotoptim.SpotOptim.SpotOptim.print_best }\n\n```python\nSpotOptim.SpotOptim.print_best(\n result=None,\n transformations=None,\n show_name=True,\n precision=4,\n)\n```\n\nPrint the best solution found during optimization.\nThis method displays the best hyperparameters and objective value in a\nformatted table. It supports custom transformations for parameters\n(e.g., converting log-scale values back to original scale).\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------------|---------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------|\n| result | [OptimizeResult](`scipy.optimize.OptimizeResult`) | Optimization result object from optimize(). If None, uses the stored best values from the optimizer. Defaults to None. | `None` |\n| transformations | list of callable | List of transformation functions to apply to each parameter. Each function takes a single value and returns the transformed value. Use None for parameters that don't need transformation. Length must match number of dimensions. Example: [None, None, lambda x: 10**x] to convert the 3rd parameter from log10 scale. Defaults to None. | `None` |\n| show_name | [bool](`bool`) | Whether to display variable names. If False, uses generic names like 'x0', 'x1', etc. Defaults to True. | `True` |\n| precision | [int](`int`) | Number of decimal places for floating point values. Defaults to 4. | `4` |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#25c3a6dc .cell execution_count=60}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x1\", \"x2\"],\n max_iter=10,\n n_initial=5\n)\nresult = opt.optimize()\nopt.print_best(result)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n\nBest Solution Found:\n--------------------------------------------------\n x1: -0.2487\n x2: -0.0076\n Objective Value: 0.0619\n Total Evaluations: 10\n```\n:::\n:::\n\n\n### print_results { #spotoptim.SpotOptim.SpotOptim.print_results }\n\n```python\nSpotOptim.SpotOptim.print_results(*args, **kwargs)\n```\n\nAlias for print(get_results_table()) for compatibility.\nPrints the table.\n\n### process_factor_bounds { #spotoptim.SpotOptim.SpotOptim.process_factor_bounds }\n\n```python\nSpotOptim.SpotOptim.process_factor_bounds()\n```\n\nProcess `bounds` to handle factor variables.\nFor dimensions with tuple bounds (factor variables), creates internal\ninteger mappings and replaces bounds with (0, n_levels-1).\nStores mappings in `self._factor_maps`: {dim_idx: {int_val: str_val}}\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|------------------------------------|\n| | [ValueError](`ValueError`) | If bounds are invalidly formatted. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#31a03340 .cell execution_count=61}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nspot = SpotOptim(fun=lambda x: x, bounds=[('red', 'green', 'blue'), (0, 10)])\nspot.process_factor_bounds()\nprint(f\"spot.bounds (should be [(0, 2), (0, 10)]): {spot.bounds}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nspot.bounds (should be [(0, 2), (0, 10)]): [(0, 2), (0, 10)]\n```\n:::\n:::\n\n\n### reinitialize_components { #spotoptim.SpotOptim.SpotOptim.reinitialize_components }\n\n```python\nSpotOptim.SpotOptim.reinitialize_components()\n```\n\nReinitialize components that were excluded during pickling.\n\n### remove_nan { #spotoptim.SpotOptim.SpotOptim.remove_nan }\n\n```python\nSpotOptim.SpotOptim.remove_nan(X, y, stop_on_zero_return=True)\n```\n\nRemove rows where y contains NaN or inf values.\nUsed in the optimize() method after function evaluations.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|---------------------|----------------------|---------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Design matrix, shape (n_samples, n_features). | _required_ |\n| y | [ndarray](`ndarray`) | Objective values, shape (n_samples,). | _required_ |\n| stop_on_zero_return | [bool](`bool`) | If True, raise error when all values are removed. | `True` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|------------------|-----------------------------------------------|\n| tuple | [tuple](`tuple`) | (X_clean, y_clean) with NaN/inf rows removed. |\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|------------------------------------------------------------|\n| | [ValueError](`ValueError`) | If all values are NaN/inf and stop_on_zero_return is True. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#faddb27f .cell execution_count=62}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nX = np.array([[1, 2], [3, 4], [5, 6]])\ny = np.array([1.0, np.nan, np.inf])\nX_clean, y_clean = opt.remove_nan(X, y, stop_on_zero_return=False)\nprint(\"Clean X:\", X_clean)\nprint(\"Clean y:\", y_clean)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nClean X: [[1 2]]\nClean y: [1.]\n```\n:::\n:::\n\n\n### repair_non_numeric { #spotoptim.SpotOptim.SpotOptim.repair_non_numeric }\n\n```python\nSpotOptim.SpotOptim.repair_non_numeric(X, var_type)\n```\n\nRound non-numeric values to integers based on variable type.\nThis method applies rounding to variables that are not continuous:\n * 'float': No rounding (continuous values)\n * 'int': Rounded to integers\n * 'factor': Rounded to integers (representing categorical values)\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|----------|----------------------|------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | X array with values to potentially round. | _required_ |\n| var_type | list of str | List with type information for each dimension. | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|---------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | X array with non-continuous values rounded to integers. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#a843236e .cell execution_count=63}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n var_type=['int', 'float'])\nX = np.array([[1.2, 2.5], [3.7, 4.1], [5.9, 6.8]])\nX_repaired = opt.repair_non_numeric(X, opt.var_type)\nprint(X_repaired)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[[1. 2.5]\n [4. 4.1]\n [6. 6.8]]\n```\n:::\n:::\n\n\n### rm_initial_design_NA_values { #spotoptim.SpotOptim.SpotOptim.rm_initial_design_NA_values }\n\n```python\nSpotOptim.SpotOptim.rm_initial_design_NA_values(X0, y0)\n```\n\nRemove NaN/inf values from initial design evaluations.\nThis method filters out design points that returned NaN or inf values\nduring initial evaluation. Unlike the sequential optimization phase where\npenalties are applied, initial design points with invalid values are\nsimply removed.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-------------------------------------------------------------------------|------------|\n| X0 | [ndarray](`ndarray`) | Initial design points in internal scale, shape (n_samples, n_features). | _required_ |\n| y0 | [ndarray](`ndarray`) | Function values at X0, shape (n_samples,). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| | [Tuple](`typing.Tuple`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`), [int](`int`)\\] | Tuple[ndarray, ndarray, int]: Filtered (X0, y0) with only finite values and the original count before filtering. X0 has shape (n_valid, n_features), y0 has shape (n_valid,), and the int is the original size. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#2041b9a3 .cell execution_count=64}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=10\n)\nX0 = np.array([[1, 2], [3, 4], [5, 6]])\ny0 = np.array([5.0, np.nan, np.inf])\nX0_clean, y0_clean, n_eval = opt.rm_initial_design_NA_values(X0, y0)\nprint(X0_clean.shape) # (1, 2)\nprint(y0_clean) # array([5.])\nprint(n_eval) # 3\n# All valid values - no filtering\nX0 = np.array([[1, 2], [3, 4]])\ny0 = np.array([5.0, 25.0])\nX0_clean, y0_clean, n_eval = opt.rm_initial_design_NA_values(X0, y0)\nprint(X0_clean.shape) # (2, 2)\nprint(n_eval) # 2\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(1, 2)\n[5.]\n3\n(2, 2)\n2\n```\n:::\n:::\n\n\n### save_experiment { #spotoptim.SpotOptim.SpotOptim.save_experiment }\n\n```python\nSpotOptim.SpotOptim.save_experiment(\n filename=None,\n prefix='experiment',\n path=None,\n overwrite=True,\n unpickleables='all',\n verbosity=0,\n)\n```\n\nSave experiment configuration to a pickle file.\n\n### save_result { #spotoptim.SpotOptim.SpotOptim.save_result }\n\n```python\nSpotOptim.SpotOptim.save_result(\n filename=None,\n prefix='result',\n path=None,\n overwrite=True,\n verbosity=0,\n)\n```\n\nSave complete optimization results to a pickle file.\n\n### select_new { #spotoptim.SpotOptim.SpotOptim.select_new }\n\n```python\nSpotOptim.SpotOptim.select_new(A, X, tolerance=0)\n```\n\nSelect rows from A that are not in X.\nUsed in suggest_next_infill_point() to avoid duplicate evaluations.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------|----------------------|------------------------------------------------|------------|\n| A | [ndarray](`ndarray`) | Array with new values. | _required_ |\n| X | [ndarray](`ndarray`) | Array with known values. | _required_ |\n| tolerance | [float](`float`) | Tolerance value for comparison. Defaults to 0. | `0` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|---------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|\n| tuple | [Tuple](`typing.Tuple`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`)\\] | A tuple containing: * ndarray: Array with unknown (new) values. * ndarray: Array with True if value is new, otherwise False. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#cc808c15 .cell execution_count=65}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nA = np.array([[1, 2], [3, 4], [5, 6]])\nX = np.array([[3, 4], [7, 8]])\nnew_A, is_new = opt.select_new(A, X)\nprint(\"New A:\", new_A)\nprint(\"Is new:\", is_new)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nNew A: [[1 2]\n [5 6]]\nIs new: [ True False True]\n```\n:::\n:::\n\n\n### sensitivity_spearman { #spotoptim.SpotOptim.SpotOptim.sensitivity_spearman }\n\n```python\nSpotOptim.SpotOptim.sensitivity_spearman()\n```\n\nCompute and print Spearman correlation between parameters and objective values.\nThis method analyzes the sensitivity of the objective function to each\nhyperparameter by computing Spearman rank correlations. For categorical\n(factor) variables, correlation is not computed as they require visual\ninspection instead.\nThe method automatically handles different parameter types:\n * Integer/float parameters: Direct correlation with objective values\n * Log-transformed parameters (log10, log, ln): Correlation in log-space\n * Factor (categorical) parameters: Skipped with informative message\nSignificance levels:\n * ***: p < 0.001 (highly significant)\n * **: p < 0.01 (significant)\n * *: p < 0.05 (marginally significant)\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#5832ff1d .cell execution_count=66}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\n\ndef test_func(X):\n # x0 has strong effect, x1 has weak effect\n X = np.atleast_2d(X)\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=10,\n n_initial=5,\n seed=42\n)\nopt.optimize()\nopt.sensitivity_spearman()\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n\nSensitivity Analysis (Spearman Correlation):\n--------------------------------------------------\n x0 : -0.188 (p=0.603)\n x1 : -0.297 (p=0.405)\n```\n:::\n:::\n\n\n#### Note {.doc-section .doc-section-note}\n\nOnly meaningful after optimize() has been called with sufficient evaluations.\n\n### set_seed { #spotoptim.SpotOptim.SpotOptim.set_seed }\n\n```python\nSpotOptim.SpotOptim.set_seed()\n```\n\nSet global random seeds for reproducibility.\nSets seeds for:\n * random\n * numpy.random\n * torch (cpu and cuda)\nOnly performs actions if self.seed is not None.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#97766e1b .cell execution_count=67}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\nspot = SpotOptim(fun=lambda x: x, bounds=[(0, 1)], seed=42)\nspot.set_seed()\nnp.random.rand() # Should be deterministic\n```\n\n::: {.cell-output .cell-output-display execution_count=67}\n```\n0.3745401188473625\n```\n:::\n:::\n\n\n### setup_dimension_reduction { #spotoptim.SpotOptim.SpotOptim.setup_dimension_reduction }\n\n```python\nSpotOptim.SpotOptim.setup_dimension_reduction()\n```\n\nSet up dimension reduction by identifying fixed dimensions.\nIdentifies dimensions where lower and upper bounds are equal in Transformed Space.\nReduces `self.bounds`, `self.lower`, `self.upper`, etc., to the Mapped Space\n(active variables only).\nThe resulting `self.bounds` defines the Transformed and Mapped Space used\nfor optimization.\nThis method identifies variables that are fixed (constant) and excludes them\nfrom the optimization process. It stores:\n * Original bounds and metadata in `all_*` attributes\n * Boolean mask of fixed dimensions in `ident`\n * Reduced bounds, types, and names for optimization\n * `red_dim` flag indicating if reduction occurred\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#3249ab7d .cell execution_count=68}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nspot = SpotOptim(fun=lambda x: x, bounds=[(1, 10), (5, 5), (0, 1)])\nprint(\"Original lower bounds:\", spot.all_lower)\nprint(\"Original upper bounds:\", spot.all_upper)\nprint(\"Fixed dimensions mask:\", spot.ident)\nprint(\"Reduced lower bounds:\", spot.lower)\nprint(\"Reduced upper bounds:\", spot.upper)\nprint(\"Reduced variable names:\", spot.var_name)\nprint(\"Is dimension reduction active?\", spot.red_dim)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nOriginal lower bounds: [1. 5. 0.]\nOriginal upper bounds: [10. 5. 1.]\nFixed dimensions mask: [False True False]\nReduced lower bounds: [1. 0.]\nReduced upper bounds: [10. 1.]\nReduced variable names: ['x0', 'x2']\nIs dimension reduction active? True\n```\n:::\n:::\n\n\n### store_mo { #spotoptim.SpotOptim.SpotOptim.store_mo }\n\n```python\nSpotOptim.SpotOptim.store_mo(y_mo)\n```\n\nStore multi-objective values in self.y_mo.\nIf multi-objective values are present (ndim==2), they are stored in self.y_mo.\nNew values are appended to existing ones. For single-objective problems,\nself.y_mo remains None.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|----------------------------------------------------------------------------------------------------------|------------|\n| y_mo | [ndarray](`ndarray`) | If multi-objective, shape (n_samples, n_objectives). If single-objective, shape (n_samples,). | _required_ |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#79764a1b .cell execution_count=69}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(\n fun=lambda X: np.column_stack([\n np.sum(X**2, axis=1),\n np.sum((X-1)**2, axis=1)\n ]),\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5\n)\ny_mo_1 = np.array([[1.0, 2.0], [3.0, 4.0]])\nopt.store_mo(y_mo_1)\nprint(f\"y_mo after first call: {opt.y_mo}\")\ny_mo_2 = np.array([[5.0, 6.0], [7.0, 8.0]])\nopt.store_mo(y_mo_2)\nprint(f\"y_mo after second call: {opt.y_mo}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ny_mo after first call: [[1. 2.]\n [3. 4.]]\ny_mo after second call: [[1. 2.]\n [3. 4.]\n [5. 6.]\n [7. 8.]]\n```\n:::\n:::\n\n\n### suggest_next_infill_point { #spotoptim.SpotOptim.SpotOptim.suggest_next_infill_point }\n\n```python\nSpotOptim.SpotOptim.suggest_next_infill_point()\n```\n\nSuggest next point to evaluate (dispatcher).\nUsed in both sequential and parallel optimization loops. This method orchestrates\nthe process of generating candidate points from the acquisition function optimizer,\nhandling any failures in the acquisition process with a fallback strategy, and\nensuring that the returned point(s) are valid and ready for evaluation.\nThe returned point is in the Transformed and Mapped Space (Internal Optimization Space).\nThis means:\n 1. Transformations (e.g., log, sqrt) have been applied.\n 2. Dimension reduction has been applied (fixed variables removed).\nProcess:\n 1. Try candidates from acquisition function optimizer.\n 2. Handle acquisition failure (fallback).\n 3. Return last attempt if all fails.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Next point(s) to evaluate in Transformed and Mapped Space. |\n| | [np](`numpy`).[ndarray](`numpy.ndarray`) | Shape is (n_infill_points, n_features). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#13bb9355 .cell execution_count=70}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n n_infill_points=2\n)\n# Need to initialize optimization state (X_, y_, surrogate)\n# Normally done inside optimize()\nnp.random.seed(0)\nopt.X_ = np.random.rand(10, 2)\nopt.y_ = np.random.rand(10)\nopt.fit_surrogate(opt.X_, opt.y_)\nx_next = opt.suggest_next_infill_point()\nx_next.shape\n```\n\n::: {.cell-output .cell-output-display execution_count=70}\n```\n(2, 2)\n```\n:::\n:::\n\n\n### to_all_dim { #spotoptim.SpotOptim.SpotOptim.to_all_dim }\n\n```python\nSpotOptim.SpotOptim.to_all_dim(X_red)\n```\n\nExpand reduced-dimensional points to full-dimensional representation.\nThis method restores points from the reduced optimization space to the\nfull-dimensional space by inserting fixed values for constant dimensions.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-------------------------------------------------------------|------------|\n| X_red | [ndarray](`ndarray`) | Points in reduced space, shape (n_samples, n_reduced_dims). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|-----------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Points in full space, shape (n_samples, n_original_dims). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#1b178667 .cell execution_count=71}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n# Create problem with one fixed dimension\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (2, 2), (-5, 5)], # x1 is fixed at 2\n max_iter=10,\n n_initial=3\n)\nX_red = np.array([[1.0, 3.0], [2.0, 4.0]]) # Only x0 and x2\nX_full = opt.to_all_dim(X_red)\nprint(X_full.shape)\nprint(X_full[:, 1])\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(2, 3)\n[2. 2.]\n```\n:::\n:::\n\n\n### to_red_dim { #spotoptim.SpotOptim.SpotOptim.to_red_dim }\n\n```python\nSpotOptim.SpotOptim.to_red_dim(X_full)\n```\n\nReduce full-dimensional points to optimization space.\nThis method removes fixed dimensions from full-dimensional points,\nextracting only the varying dimensions used in optimization.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------------------|------------|\n| X_full | [ndarray](`ndarray`) | Points in full space, shape (n_samples, n_original_dims). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|-------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Points in reduced space, shape (n_samples, n_reduced_dims). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#d259a0b5 .cell execution_count=72}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n# Create problem with one fixed dimension\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (2, 2), (-5, 5)], # x1 is fixed at 2\n max_iter=10,\n n_initial=3\n)\nX_full = np.array([[1.0, 2.0, 3.0], [4.0, 2.0, 5.0]])\nX_red = opt.to_red_dim(X_full)\nprint(X_red.shape)\nprint(np.array_equal(X_red, np.array([[1.0, 3.0], [4.0, 5.0]])))\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(2, 2)\nTrue\n```\n:::\n:::\n\n\n### transform_X { #spotoptim.SpotOptim.SpotOptim.transform_X }\n\n```python\nSpotOptim.SpotOptim.transform_X(X)\n```\n\nTransform parameter array from original (natural) to internal scale.\nConverts from natural space (Original) to transformed space (full dimension).\nDoes NOT handle dimension reduction (mapping).\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-------------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Array in Natural Space, shape (n_samples, n_features) | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|---------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Array in Transformed Space (Full Dimension) |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#394b7845 .cell execution_count=73}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\nfrom spotoptim.function import sphere\nspot = SpotOptim(fun=sphere, bounds=[(1, 10)], var_trans=['log10'])\nX_orig = np.array([[1], [10], [100]])\nspot.transform_X(X_orig)\n```\n\n::: {.cell-output .cell-output-display execution_count=73}\n```\narray([[0],\n [1],\n [2]])\n```\n:::\n:::\n\n\n### transform_bounds { #spotoptim.SpotOptim.SpotOptim.transform_bounds }\n\n```python\nSpotOptim.SpotOptim.transform_bounds()\n```\n\nTransform bounds from original to internal scale.\nUpdates `self.bounds` (and `self.lower`, `self.upper`) from Natural Space\nto Transformed Space. Calls `transform_value` for each bound and converts\nnumpy types to Python native types (`int` or `float` based on `var_type`).\nHandles also reversed bounds, e.g., as an effect of `reciprocal` transformation.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Notes {.doc-section .doc-section-notes}\n\nUses settings in `self.var_trans`. It can be one of `id`, `log10`, `log`, `ln`, `sqrt`,\n`exp`, `square`, `cube`, `inv`, `reciprocal`, or `None`. Also supports dynamic\nstrings like `log(x)`, `sqrt(x)`, `pow(x, p)`.\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#b2b77c21 .cell execution_count=74}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nimport numpy as np\nspot = SpotOptim(fun=sphere, bounds=[(1, 10), (0.1, 100)])\nspot.var_trans = ['log10', 'sqrt']\nspot.transform_bounds()\nprint(f\"spot.bounds: {spot.bounds}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nspot.bounds: [(0.0, 1.0), (0.31622776601683794, 10.0)]\n```\n:::\n:::\n\n\n### transform_value { #spotoptim.SpotOptim.SpotOptim.transform_value }\n\n```python\nSpotOptim.SpotOptim.transform_value(x, trans)\n```\n\nApply transformation to a single float value.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|-----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|\n| x | [float](`float`) | Value to transform | _required_ |\n| trans | [Optional](`typing.Optional`)\\[[str](`str`)\\] | Transformation name. Can be one of `id`, `log10`, `log`, `ln`, `sqrt`, `exp`, `square`, `cube`, `inv`, `reciprocal`, or `None`. Also supports dynamic strings like `log(x)`, `sqrt(x)`, `pow(x, p)`. | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|------------------|-------------------|\n| | [float](`float`) | Transformed value |\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|--------------------------------------------|\n| | [TypeError](`TypeError`) | If x is not a float. |\n| | [ValueError](`ValueError`) | If an unknown transformation is specified. |\n\n#### Notes {.doc-section .doc-section-notes}\n\nSee also inverse_transform_value.\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#7cc60ddc .cell execution_count=75}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nspot = SpotOptim(fun=sphere, bounds=[(1, 10)])\nspot.transform_value(10, 'log10')\nspot.transform_value(100, 'log(x)')\n```\n\n::: {.cell-output .cell-output-display execution_count=75}\n```\nnp.float64(4.605170185988092)\n```\n:::\n:::\n\n\n### update_repeats_infill_points { #spotoptim.SpotOptim.SpotOptim.update_repeats_infill_points }\n\n```python\nSpotOptim.SpotOptim.update_repeats_infill_points(x_next)\n```\n\nRepeat infill point for noisy function evaluation. Used in the sequential_loop.\nFor noisy objective functions (repeats_surrogate > 1), creates multiple\ncopies of the suggested point for repeated evaluation. Otherwise, returns\nthe point in 2D array format.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|----------------------------------------------|------------|\n| x_next | [ndarray](`ndarray`) | Next point to evaluate, shape (n_features,). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|---------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Points to evaluate, shape (repeats_surrogate, n_features) or (1, n_features) if repeats_surrogate == 1. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#fb1b62bc .cell execution_count=76}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere, noisy_sphere\n# Without repeats\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n repeats_surrogate=1\n)\nx_next = np.array([1.0, 2.0])\nx_repeated = opt.update_repeats_infill_points(x_next)\nprint(x_repeated.shape)\n\n# With repeats for noisy function\nopt_noisy = SpotOptim(\n fun=noisy_sphere,\n bounds=[(-5, 5), (-5, 5)],\n repeats_surrogate=3\n)\nx_next = np.array([1.0, 2.0])\nx_repeated = opt_noisy.update_repeats_infill_points(x_next)\nprint(x_repeated.shape)\n# All three copies should be identical\nnp.all(x_repeated[0] == x_repeated[1])\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(1, 2)\n(3, 2)\n```\n:::\n\n::: {.cell-output .cell-output-display execution_count=76}\n```\nnp.True_\n```\n:::\n:::\n\n\n### update_stats { #spotoptim.SpotOptim.SpotOptim.update_stats }\n\n```python\nSpotOptim.SpotOptim.update_stats()\n```\n\nUpdate optimization statistics.\nUpdates various statistics related to the optimization progress:\n * `min_y`: Minimum y value found so far\n * `min_X`: X value corresponding to minimum y\n * `counter`: Total number of function evaluations\n\n#### Notes {.doc-section .doc-section-notes}\n\n`success_rate` is updated separately via `update_success_rate()` method, which is called after each batch of function evaluations.\n\nIf \"noise\" is True (`repeats_initial > 1` or `repeats_surrogate > 1`), additionally computes:\n * `mean_X`: Unique design points (aggregated from repeated evaluations)\n * `mean_y`: Mean y values per design point\n * `var_y`: Variance of y values per design point\n * `min_mean_X`: X value of the best mean y value\n * `min_mean_y`: Best mean y value\n * `min_var_y`: Variance of the best mean y value\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#f798213c .cell execution_count=77}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n# Without noise\nopt = SpotOptim(fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10, n_initial=5)\nopt.optimize()\nprint(\"SpotOptim stats without noise:\")\nprint(f\"opt.X_: {opt.X_}\")\nprint(f\"opt.y_: {opt.y_}\")\nprint(f\"opt.min_y: {opt.min_y}\")\nprint(f\"opt.min_X: {opt.min_X}\")\nprint(f\"opt.counter: {opt.counter}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSpotOptim stats without noise:\nopt.X_: [[ 1.7536098 -2.85254155]\n [ 4.64692713 -4.87048515]\n [-4.99069856 1.39144855]\n [-1.52382491 0.82921902]\n [-0.5652495 4.777974 ]\n [-1.41517905 4.55159715]\n [-1.41135148 0.74992908]\n [-0.52273055 -0.6508731 ]\n [-0.49729275 -0.29408537]\n [ 0.01195296 -0.02663126]]\nopt.y_: [1.12121406e+01 4.53155573e+01 2.68432012e+01 3.00964655e+00\n 2.31485425e+01 2.27197683e+01 2.55430662e+00 6.96883019e-01\n 3.33786285e-01 8.52097097e-04]\nopt.min_y: 0.0008520970970148645\nopt.min_X: [ 0.01195296 -0.02663126]\nopt.counter: 10\n```\n:::\n:::\n\n\n::: {#cacb2efa .cell execution_count=78}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import noisy_sphere\n# With noise\nopt_noise = SpotOptim(fun=noisy_sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n repeats_surrogate=2,\n repeats_initial=2)\nopt_noise.optimize()\nprint(\"SpotOptim stats with noise:\")\nprint(f\"opt_noise.X_: {opt_noise.X_}\")\nprint(f\"opt_noise.y_: {opt_noise.y_}\")\nprint(f\"opt_noise.min_y: {opt_noise.min_y}\")\nprint(f\"opt_noise.min_X: {opt_noise.min_X}\")\nprint(f\"opt_noise.counter: {opt_noise.counter}\")\nprint(f\"opt_noise.mean_X: {opt_noise.mean_X}\")\nprint(f\"opt_noise.mean_y: {opt_noise.mean_y}\")\nprint(f\"opt_noise.var_y: {opt_noise.var_y}\")\nprint(f\"opt_noise.min_mean_X: {opt_noise.min_mean_X}\")\nprint(f\"opt_noise.min_mean_y: {opt_noise.min_mean_y}\")\nprint(f\"opt_noise.min_var_y: {opt_noise.min_var_y}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSpotOptim stats with noise:\nopt_noise.X_: [[-2.65526809 1.77019005]\n [-2.65526809 1.77019005]\n [ 3.95344135 -2.6651882 ]\n [ 3.95344135 -2.6651882 ]\n [-4.17695613 4.51758594]\n [-4.17695613 4.51758594]\n [-0.77907893 0.92562589]\n [-0.77907893 0.92562589]\n [ 2.4226093 -3.89232436]\n [ 2.4226093 -3.89232436]\n [-0.91911362 0.83459436]\n [-0.91911362 0.83459436]\n [-0.57276255 0.85384653]\n [-0.57276255 0.85384653]\n [ 0.10433426 0.39590289]\n [ 0.10433426 0.39590289]\n [ 0.06442638 0.43841988]\n [ 0.06442638 0.43841988]\n [ 0.15448329 -0.58419544]\n [ 0.15448329 -0.58419544]]\nopt_noise.y_: [ 1.01528238e+01 1.00999924e+01 2.26322435e+01 2.29010843e+01\n 3.77763166e+01 3.78023847e+01 1.50033215e+00 1.59352979e+00\n 2.10673362e+01 2.12951602e+01 1.66805876e+00 1.59126742e+00\n 9.98187936e-01 1.01226433e+00 1.22748032e-01 -6.79404257e-03\n 2.81415818e-01 1.12450331e-01 2.68032337e-01 5.07781090e-01]\nopt_noise.min_y: -0.006794042574277848\nopt_noise.min_X: [0.10433426 0.39590289]\nopt_noise.counter: 20\nopt_noise.mean_X: [[-4.17695613 4.51758594]\n [-2.65526809 1.77019005]\n [-0.91911362 0.83459436]\n [-0.77907893 0.92562589]\n [-0.57276255 0.85384653]\n [ 0.06442638 0.43841988]\n [ 0.10433426 0.39590289]\n [ 0.15448329 -0.58419544]\n [ 2.4226093 -3.89232436]\n [ 3.95344135 -2.6651882 ]]\nopt_noise.mean_y: [37.78935067 10.12640812 1.62966309 1.54693097 1.00522613 0.19693307\n 0.05797699 0.38790671 21.18124823 22.76666389]\nopt_noise.var_y: [1.69886138e-04 6.97789966e-04 1.47422756e-03 2.17145039e-03\n 4.95362454e-05 7.13733398e-03 4.19528726e-03 1.43698661e-02\n 1.29759436e-02 1.80688502e-02]\nopt_noise.min_mean_X: [0.10433426 0.39590289]\nopt_noise.min_mean_y: 0.05797699459814497\nopt_noise.min_var_y: 0.004195287256391378\n```\n:::\n:::\n\n\n### update_storage { #spotoptim.SpotOptim.SpotOptim.update_storage }\n\n```python\nSpotOptim.SpotOptim.update_storage(X_new, y_new)\n```\n\nUpdate storage (`X_`, `y_`) with new evaluation points.\nAppends new design points and their function values to the storage arrays.\nPoints are converted from internal scale to original scale before storage.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------------------------|------------|\n| X_new | [ndarray](`ndarray`) | New design points in internal scale, shape (n_new, n_features). | _required_ |\n| y_new | [ndarray](`ndarray`) | Function values at X_new, shape (n_new,). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#fd54bcec .cell execution_count=79}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5)\n# Initialize with some data\nopt.X_ = np.array([[1, 2], [3, 4]])\nopt.y_ = np.array([5.0, 25.0])\nprint(\"Initial storage:\")\nprint(opt.X_)\nprint(opt.y_)\n# Add new points\nX_new = np.array([[0, 1], [2, 3]])\ny_new = np.array([1.0, 13.0])\nopt.update_storage(X_new, y_new)\nprint(\"Updated storage:\")\nprint(opt.X_)\nprint(opt.y_)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nInitial storage:\n[[1 2]\n [3 4]]\n[ 5. 25.]\nUpdated storage:\n[[1 2]\n [3 4]\n [0 1]\n [2 3]]\n[ 5. 25. 1. 13.]\n```\n:::\n:::\n\n\n### update_success_rate { #spotoptim.SpotOptim.SpotOptim.update_success_rate }\n\n```python\nSpotOptim.SpotOptim.update_success_rate(y_new)\n```\n\nUpdate the rolling success rate of the optimization process.\nA success is counted only if the new value is better (smaller) than the best\nfound y value so far. The success rate is calculated based on the last\n`window_size` successes.\nImportant: This method should be called BEFORE updating self.y_ to correctly\ntrack improvements against the previous best value.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|------------------------------------------------------------------|------------|\n| y_new | [ndarray](`ndarray`) | The new function values to consider for the success rate update. | _required_ |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#5efccf3d .cell execution_count=80}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10, n_initial=5)\nprint(opt.success_rate)\nopt.X_ = np.array([[1, 2], [3, 4], [0, 1]])\nopt.y_ = np.array([5.0, 3.0, 2.0])\nopt.update_success_rate(np.array([1.5, 2.5]))\nprint(opt.success_rate)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n0.0\n0.5\n```\n:::\n:::\n\n\n### validate_x0 { #spotoptim.SpotOptim.SpotOptim.validate_x0 }\n\n```python\nSpotOptim.SpotOptim.validate_x0(x0)\n```\n\nValidate and process starting point x0. Called in `__init__` and `optimize`.\nThis method checks that x0:\n * Is a numpy array\n * Has the correct number of dimensions\n * Has values within bounds (in original scale)\n * Is properly transformed to internal scale\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|-----------------------------------|----------------------------------|------------|\n| x0 | [array](`array`) - [like](`like`) | Starting point in original scale | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|---------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Validated and transformed x0 in internal scale, shape (n_features,) |\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|------------------|\n| | [ValueError](`ValueError`) | If x0 is invalid |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#66799645 .cell execution_count=81}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (5,5), (-10, 10)],\n x0=np.array([1.0, 5.0, 9.0]),\n var_trans=[\"log10\", \"id\", \"sqrt\"]\n)\n# x0 is validated during initialization and transformed to internal scale\nprint(f\"x0 in internal scale: {opt.x0}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nx0 in internal scale: [0. 3.]\n```\n:::\n:::\n\n\n", + "markdown": "---\ntitle: SpotOptim.SpotOptim\n---\n\n\n\n```python\nSpotOptim.SpotOptim(\n fun,\n bounds=None,\n max_iter=20,\n n_initial=10,\n surrogate=None,\n acquisition='y',\n var_type=None,\n var_name=None,\n var_trans=None,\n tolerance_x=None,\n max_time=np.inf,\n repeats_initial=1,\n repeats_surrogate=1,\n ocba_delta=0,\n tensorboard_log=False,\n tensorboard_path=None,\n tensorboard_clean=False,\n fun_mo2so=None,\n seed=None,\n verbose=False,\n warnings_filter='ignore',\n n_infill_points=1,\n max_surrogate_points=None,\n selection_method='distant',\n acquisition_failure_strategy='random',\n penalty=False,\n penalty_val=None,\n acquisition_fun_return_size=3,\n acquisition_optimizer='differential_evolution',\n restart_after_n=100,\n restart_inject_best=True,\n max_restarts=None,\n x0=None,\n de_x0_prob=0.1,\n tricands_fringe=False,\n prob_de_tricands=0.8,\n window_size=None,\n min_tol_metric='chebyshev',\n prob_surrogate=None,\n acquisition_optimizer_kwargs=None,\n args=(),\n kwargs=None,\n)\n```\n\nSPOT optimizer compatible with scipy.optimize interface.\n\n## Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|------------------------------|-------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------|\n| fun | [callable](`callable`) | Objective function to minimize. Should accept array of shape (n_samples, n_features). | _required_ |\n| bounds | list of tuple | Bounds for each dimension as [(low, high), ...]. | `None` |\n| max_iter | [int](`int`) | Maximum number of total function evaluations (including initial design). For example, max_iter=30 with n_initial=10 will perform 10 initial evaluations plus 20 sequential optimization iterations. Defaults to 20. | `20` |\n| n_initial | [int](`int`) | Number of initial design points. Defaults to 10. | `10` |\n| surrogate | [object](`object`) | Surrogate model with scikit-learn interface (fit/predict methods). If None, uses a Gaussian Process Regressor with Matern kernel. Default configuration:: * `from sklearn.gaussian_process import GaussianProcessRegressor` * `from sklearn.gaussian_process.kernels import Matern, ConstantKernel` * `kernel = ConstantKernel(1.0, (1e-2, 1e12)) * Matern(length_scale=1.0, length_scale_bounds=(1e-4, 1e2), nu=2.5)` * `surrogate = GaussianProcessRegressor(kernel=kernel, n_restarts_optimizer=100)` Alternative surrogates can be provided, including SpotOptim's Kriging model, Random Forests, or any scikit-learn compatible regressor. See Examples section. Defaults to None (uses default Gaussian Process configuration). | `None` |\n| acquisition | [str](`str`) | Acquisition function ('ei', 'y', 'pi'). Defaults to 'y'. | `'y'` |\n| var_type | list of str | Variable types for each dimension. Supported types: * 'float': Python floats, continuous optimization (no rounding) * 'int': Python int, float values will be rounded to integers * 'factor': Unordered categorical data, internally mapped to int values (e.g., \"red\"->0, \"green\"->1, etc.) Defaults to None (which sets all dimensions to 'float'). | `None` |\n| var_name | list of str | Variable names for each dimension. If None, uses default names ['x0', 'x1', 'x2', ...]. Defaults to None. | `None` |\n| tolerance_x | [float](`float`) | Minimum distance between points. Defaults to np.sqrt(np.spacing(1)) | `None` |\n| var_trans | list of str | Variable transformations for each dimension. Supported: It can be one of `id`, `log10`, `log`, `ln`, `sqrt`, `exp`, `square`, `cube`, `inv`, `reciprocal`, or `None`. Also supports dynamic strings like `log(x)`, `sqrt(x)`, `pow(x, p)`. Defaults to None (no transformations). | `None` |\n| max_time | [float](`float`) | Maximum runtime in minutes. If np.inf (default), no time limit. The optimization terminates when either max_iter evaluations are reached OR max_time minutes have elapsed, whichever comes first. Defaults to np.inf. | `np.inf` |\n| repeats_initial | [int](`int`) | Number of times to evaluate each initial design point. Useful for noisy objective functions. If > 1, noise handling is activated and statistics (mean, variance) are tracked. Defaults to 1. | `1` |\n| repeats_surrogate | [int](`int`) | Number of times to evaluate each surrogate-suggested point. Useful for noisy objective functions. If > 1, noise handling is activated and statistics (mean, variance) are tracked. Defaults to 1. | `1` |\n| ocba_delta | [int](`int`) | Number of additional evaluations to allocate using Optimal Computing Budget Allocation (OCBA) when noise handling is active. OCBA determines which existing design points should be re-evaluated to best distinguish between alternatives. Only used when repeats_surrogate > 1 and ocba_delta > 0. Requires at least 3 design points with variance information. Defaults to 0 (no OCBA). | `0` |\n| tensorboard_log | [bool](`bool`) | Enable TensorBoard logging. If True, optimization metrics and hyperparameters are logged to TensorBoard. View logs by running: `tensorboard --logdir=` in a separate terminal. Defaults to False. | `False` |\n| tensorboard_path | [str](`str`) | Path for TensorBoard log files. If None and tensorboard_log is True, creates a default path: runs/spotoptim_YYYYMMDD_HHMMSS. Defaults to None. | `None` |\n| tensorboard_clean | [bool](`bool`) | If True, removes old TensorBoard logs before starting optimization so every run begins with a fresh dashboard. With tensorboard_path set, the configured directory itself is removed (and re-created empty by the writer); without a path, all subdirectories of the default 'runs' folder are removed. Use with caution as this permanently deletes the affected log directories. Defaults to False. | `False` |\n| fun_mo2so | [callable](`callable`) | Function to convert multi-objective values to single-objective. Takes an array of shape (n_samples, n_objectives) and returns array of shape (n_samples,). If None and objective function returns multi-objective values, uses first objective. Defaults to None. | `None` |\n| seed | [int](`int`) | Random seed for reproducibility. Defaults to None. | `None` |\n| verbose | [bool](`bool`) | Print progress information. Defaults to False. | `False` |\n| warnings_filter | [Literal](`typing.Literal`)\\[\\'default\\', \\'error\\', \\'ignore\\'\\] | Filter for warnings. One of \"error\", \"ignore\", \"always\", \"all\", \"default\", \"module\", or \"once\". Defaults to \"ignore\". | `'ignore'` |\n| n_infill_points | [int](`int`) | Number of infill points to suggest at each iteration. Defaults to 1. If > 1, multiple distinct points are proposed using the optimizer and fallback strategies. | `1` |\n| max_surrogate_points | [int](`int`) | Maximum number of points to use for surrogate model fitting. If None, all points are used. If the number of evaluated points exceeds this limit, a subset is selected using the selection method. Defaults to None. | `None` |\n| selection_method | [str](`str`) | Method for selecting points when max_surrogate_points is exceeded. Options: 'distant' (Select points that are distant from each other via K-means clustering) or 'best' (Select all points from the cluster with the best mean objective value). Defaults to 'distant'. | `'distant'` |\n| acquisition_failure_strategy | [str](`str`) | Strategy for handling acquisition function failures. Options: 'random' (space-filling design via Latin Hypercube Sampling) Defaults to 'random'. | `'random'` |\n| penalty | [bool](`bool`) | Whether to use penalty for handling NaN/inf values in objective function evaluations. Defaults to False. | `False` |\n| penalty_val | [float](`float`) | Penalty value to replace NaN/inf values in objective function evaluations. When the objective function returns NaN or inf, these values are replaced with penalty plus a small random noise (sampled from N(0, 0.1)) to avoid identical penalty values. This allows optimization to continue despite occasional function evaluation failures. Defaults to None. | `None` |\n| acquisition_fun_return_size | [int](`int`) | Number of top candidates to return from acquisition function optimization. Defaults to 3. | `3` |\n| acquisition_optimizer | [str](`str`) or [callable](`callable`) | Optimizer to use for maximizing acquisition function. Can be \"differential_evolution\" (default) or any method name supported by scipy.optimize.minimize (e.g., \"Nelder-Mead\", \"L-BFGS-B\"). Can also be a callable with signature compatible with scipy.optimize.minimize (fun, x0, bounds, ...). A specific version is \"de_tricands\", which combines DE with Tricands. It can be parameterized with \"prob_de_tricands\" (probability of using DE). Defaults to \"differential_evolution\". | `'differential_evolution'` |\n| acquisition_optimizer_kwargs | [dict](`dict`) | Kwargs passed to the acquisition function optimizer and GPR surrogate optimizer. Defaults to {'maxiter': 10000, 'gtol': 1e-9}. | `None` |\n| restart_after_n | [int](`int`) | Number of consecutive iterations with zero success rate before triggering a restart. Defaults to 100. | `100` |\n| restart_inject_best | [bool](`bool`) | Whether to inject the best solution found so far as a starting point for the next restart. Defaults to True. | `True` |\n| max_restarts | [Optional](`typing.Optional`)\\[[int](`int`)\\] | Patience-based early-stopping threshold. When set to a non-negative integer ``N``, the optimizer terminates after ``N`` consecutive restarts that fail to improve the best objective value. The returned :class:`scipy.optimize.OptimizeResult` has ``success=True`` and a message of the form ``\"Optimization early stopped: no improvement for N consecutive restarts\"``. This rule complements ``restart_after_n`` and mirrors the ``no_progress_loss`` pattern in Hyperopt and plateau-based stopping in Ray Tune and SMAC. ``None`` (default) disables the rule so the optimizer runs until ``max_iter`` or ``max_time`` is reached. | `None` |\n| x0 | [array](`array`) - [like](`like`) | Starting point for optimization, shape (n_features,). If provided, this point will be evaluated first and included in the initial design. The point should be within the bounds and will be validated before use. Defaults to None (no starting point, uses only LHS design). | `None` |\n| de_x0_prob | [float](`float`) | Probability of using the best point as starting point for differential evolution. Defaults to 0.1. | `0.1` |\n| tricands_fringe | [bool](`bool`) | Whether to use the fringe of the design space for the initial design. Defaults to False. | `False` |\n| prob_de_tricands | [float](`float`) | Probability of using differential evolution as an optimizer on the surrogate model. 1 - prob_de_tricands is the probability of using tricands. Defaults to 0.8. | `0.8` |\n| window_size | [int](`int`) | Window size for success rate calculation. | `None` |\n| min_tol_metric | [str](`str`) | Distance metric used when checking `tolerance_x` for duplicate detection. Default is \"chebyshev\". Supports all metrics from scipy.spatial.distance.cdist, including: * \"chebyshev\": L-infinity distance (hypercube). Default. Matches previous behavior. * \"euclidean\": L2 distance (hypersphere). * \"minkowski\": Lp distance (default p=2). * \"cityblock\": Manhattan/L1 distance. * \"cosine\": Cosine distance. * \"correlation\": Correlation distance. * \"canberra\", \"braycurtis\", \"sqeuclidean\", etc. | `'chebyshev'` |\n\n## Attributes {.doc-section .doc-section-attributes}\n\n| Name | Type | Description |\n|------------------------------|-------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|\n| X_ | [ndarray](`ndarray`) | All evaluated points, shape (n_samples, n_features). |\n| y_ | [ndarray](`ndarray`) | Function values at X_, shape (n_samples,). For multi-objective problems, these are the converted single-objective values. |\n| y_mo | [ndarray](`ndarray`) or None | Multi-objective function values, shape (n_samples, n_objectives). None for single-objective problems. |\n| best_x_ | [ndarray](`ndarray`) | Best point found, shape (n_features,). |\n| best_y_ | [float](`float`) | Best function value found. |\n| n_iter_ | [int](`int`) | Number of iterations performed. This is not the same as counter. Provided for compatibility with scipy.optimize routines. |\n| counter | [int](`int`) | Total number of function evaluations. |\n| success_rate | [float](`float`) | Rolling success rate over the last window_size evaluations. A success is counted when a new evaluation improves upon the best value found so far. |\n| warnings_filter | [Literal](`typing.Literal`)\\[\\'default\\', \\'error\\', \\'ignore\\'\\] | Filter for warnings during optimization. |\n| max_surrogate_points | [int](`int`) or None | Maximum number of points for surrogate fitting. |\n| selection_method | [str](`str`) | Point selection method. |\n| acquisition_failure_strategy | [str](`str`) | Strategy for handling acquisition failures ('random'). |\n| mean_X | [ndarray](`ndarray`) or None | Aggregated unique design points (if repeats_surrogate > 1). |\n| mean_y | [ndarray](`ndarray`) or None | Mean y values per design point (if repeats_surrogate > 1). |\n| var_y | [ndarray](`ndarray`) or None | Variance of y values per design point (if repeats_surrogate > 1). |\n| min_mean_X | [ndarray](`ndarray`) or None | X value of best mean y (if repeats_surrogate > 1). |\n| min_mean_y | [float](`float`) or None | Best mean y value (if repeats_surrogate > 1). |\n| min_var_y | [float](`float`) or None | Variance of best mean y (if repeats_surrogate > 1). |\n| de_x0_prob | [float](`float`) | Probability of using the best point as starting point for differential evolution. |\n| tricands_fringe | [bool](`bool`) | Whether to use the fringe of the design space for the initial design. |\n| prob_de_tricands | [float](`float`) | Probability of using differential evolution as an optimizer on the surrogate model. |\n\n## Examples {.doc-section .doc-section-examples}\n\n\n::: {#4ecb6548 .cell execution_count=1}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 1: Basic usage (deterministic function)\nbounds = [(-5, 5), (-5, 5)]\noptimizer = SpotOptim(fun=objective, bounds=bounds, max_iter=10, n_initial=5, verbose=True)\nresult = optimizer.optimize()\nprint(\"Best x:\", result.x)\nprint(\"Best f(x):\", result.fun)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 4.636459\nIter 1 | Best: 4.636459 | Curr: 5.315778 | Rate: 0.00 | Evals: 60.0%\nIter 2 | Best: 4.636313 | Rate: 0.50 | Evals: 70.0%\nIter 3 | Best: 4.281456 | Rate: 0.67 | Evals: 80.0%\nIter 4 | Best: 2.711596 | Rate: 0.75 | Evals: 90.0%\nIter 5 | Best: 0.301565 | Rate: 0.80 | Evals: 100.0%\nBest x: [-0.40475083 -0.37113639]\nBest f(x): 0.3015654589641591\n```\n:::\n:::\n\n\n::: {#93ce63d1 .cell execution_count=2}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 2: With custom variable names\noptimizer = SpotOptim(\n fun=objective,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"param1\", \"param2\"],\n max_iter=10,\n n_initial=5\n)\nresult = optimizer.optimize()\n# Ensure we can use custom names in plots\noptimizer.plot_surrogate(show=False)\n```\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim.SpotOptim_files/figure-html/cell-3-output-1.png){width=1125 height=950}\n:::\n:::\n\n\n::: {#acf5b17c .cell execution_count=3}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\n# Example 3: Noisy function with repeated evaluations\ndef noisy_objective(X):\n base = np.sum(X**2, axis=1)\n noise = np.random.normal(0, 0.1, size=base.shape)\n return base + noise\n\noptimizer = SpotOptim(\n fun=noisy_objective,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5,\n repeats_initial=1, # Evaluate each initial point once\n repeats_surrogate=2, # Evaluate each new point twice\n seed=42, # For reproducibility\n verbose=True\n)\nresult = optimizer.optimize()\n\n# Access noise statistics\nprint(\"Unique design points:\", optimizer.mean_X.shape[0])\nprint(\"Best mean value:\", optimizer.min_mean_y)\nprint(\"Variance at best point:\", optimizer.min_var_y)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 3.403652, mean best: f(x) = 3.403652\nIter 1 | Best: 3.279049 | Rate: 0.50 | Evals: 70.0% | Mean Best: 3.369716\nIter 2 | Best: 3.279049 | Curr: 3.392849 | Rate: 0.25 | Evals: 90.0% | Mean Curr: 3.454694\nIter 3 | Best: 1.563282 | Rate: 0.50 | Evals: 110.0% | Mean Best: 1.613581\nUnique design points: 8\nBest mean value: 1.6135806237005457\nVariance at best point: 0.002529978015323257\n```\n:::\n:::\n\n\n::: {#7f26784f .cell execution_count=4}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef noisy_objective(X):\n base = np.sum(X**2, axis=1)\n noise = np.random.normal(0, 0.1, size=base.shape)\n return base + noise\n\n# Example 4: Noisy function with OCBA (Optimal Computing Budget Allocation)\noptimizer_ocba = SpotOptim(\n fun=noisy_objective,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=20,\n n_initial=5,\n repeats_initial=2, # Initial repeats\n repeats_surrogate=1, # Surrogate repeats\n ocba_delta=3, # Allocate 3 additional evaluations per iteration\n seed=42,\n verbose=True\n)\nresult = optimizer_ocba.optimize()\n\n# OCBA intelligently re-evaluates promising points to reduce uncertainty\nprint(\"Total evaluations:\", result.nfev)\nprint(\"Unique design points:\", optimizer_ocba.mean_X.shape[0])\nprint(\"Best mean value:\", optimizer_ocba.min_mean_y)\nprint(\"Variance at best point:\", optimizer_ocba.min_var_y)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 3.328092, mean best: f(x) = 3.368681\n\nIn get_ocba():\nmeans: [25.90094202 19.61660056 23.96405211 3.36868097 10.79578138]\nvars: [6.73858271e-13 2.56053422e-03 1.00799409e-03 1.64745915e-03\n 1.91555606e-03]\ndelta: 3\nn_designs: 5\nRatios: [3.82210611e-11 2.79305049e-01 6.84325095e-02 9.58065217e-01\n 1.00000000e+00]\nBest: 3, Second best: 4\n OCBA: Adding 3 re-evaluation(s)\nIter 1 | Best: 3.103418 | Rate: 0.75 | Evals: 70.0% | Mean Best: 3.103418\nIter 2 | Best: 3.103418 | Curr: 3.354605 | Rate: 0.60 | Evals: 75.0% | Mean Curr: 3.354605\nIter 3 | Best: 1.613729 | Rate: 0.67 | Evals: 80.0% | Mean Best: 1.613729\nIter 4 | Best: 1.230181 | Rate: 0.71 | Evals: 85.0% | Mean Best: 1.230181\nIter 5 | Best: 0.449320 | Rate: 0.75 | Evals: 90.0% | Mean Best: 0.449320\nIter 6 | Best: 0.367163 | Rate: 0.78 | Evals: 95.0% | Mean Best: 0.367163\nIter 7 | Best: 0.367163 | Curr: 0.518496 | Rate: 0.70 | Evals: 100.0% | Mean Curr: 0.518496\nTotal evaluations: 20\nUnique design points: 12\nBest mean value: 0.3671633104119547\nVariance at best point: 0.0\n```\n:::\n:::\n\n\n::: {#d4b7c883 .cell execution_count=5}\n``` {.python .cell-code}\nimport numpy as np\nimport shutil\nimport os\nfrom spotoptim import SpotOptim\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 5: With TensorBoard logging\ntb_dir = \"runs/my_optimization\"\noptimizer_tb = SpotOptim(\n fun=objective,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5,\n tensorboard_log=True, # Enable TensorBoard\n tensorboard_path=tb_dir, # Optional custom path\n verbose=True\n)\nresult = optimizer_tb.optimize()\n\n# View logs in browser: tensorboard --logdir=runs/my_optimization\nprint(\"Logs saved to:\", optimizer_tb.tensorboard_path)\n\n# Cleanup log dir\nif os.path.exists(tb_dir):\n shutil.rmtree(tb_dir)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging enabled: runs/my_optimization\nInitial best: f(x) = 0.412239\nIter 1 | Best: 0.407719 | Rate: 1.00 | Evals: 60.0%\nIter 2 | Best: 0.180004 | Rate: 1.00 | Evals: 70.0%\nIter 3 | Best: 0.023776 | Rate: 1.00 | Evals: 80.0%\nIter 4 | Best: 0.007910 | Rate: 1.00 | Evals: 90.0%\nIter 5 | Best: 0.001229 | Rate: 1.00 | Evals: 100.0%\nTensorBoard writer closed. View logs with: tensorboard --logdir=runs/my_optimization\nLogs saved to: runs/my_optimization\n```\n:::\n:::\n\n\n::: {#25c90219 .cell execution_count=6}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.surrogate import Kriging\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 6: Using SpotOptim's Kriging surrogate\nkriging_model = Kriging(\n noise=1e-10, # Regularization parameter\n kernel='gauss', # Gaussian/RBF kernel\n min_theta=-3.0, # Min log10(theta) bound\n max_theta=2.0, # Max log10(theta) bound\n seed=42\n)\noptimizer_kriging = SpotOptim(\n fun=objective,\n bounds=[(-5, 5), (-5, 5)],\n surrogate=kriging_model,\n max_iter=10,\n n_initial=5,\n seed=42,\n verbose=True\n)\nresult = optimizer_kriging.optimize()\nprint(\"Best solution found:\", result.x)\nprint(\"Best value:\", result.fun)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 3.251349\nIter 1 | Best: 3.251349 | Curr: 4.425619 | Rate: 0.00 | Evals: 60.0%\nIter 2 | Best: 1.617693 | Rate: 0.50 | Evals: 70.0%\nIter 3 | Best: 1.617693 | Curr: 18.716279 | Rate: 0.33 | Evals: 80.0%\nIter 4 | Best: 0.839564 | Rate: 0.50 | Evals: 90.0%\nIter 5 | Best: 0.102879 | Rate: 0.60 | Evals: 100.0%\nBest solution found: [0.00128033 0.32074518]\nBest value: 0.10287911055669191\n```\n:::\n:::\n\n\n::: {#0eeb445f .cell execution_count=7}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom sklearn.gaussian_process import GaussianProcessRegressor\nfrom sklearn.gaussian_process.kernels import RBF, ConstantKernel, WhiteKernel\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 7: Using sklearn Gaussian Process with custom kernel\n# Custom kernel: constant * RBF + white noise\ncustom_kernel = ConstantKernel(1.0, (1e-2, 1e2)) * RBF(\n length_scale=1.0, length_scale_bounds=(1e-1, 10.0)\n) + WhiteKernel(noise_level=1e-5, noise_level_bounds=(1e-10, 1e-1))\n\ngp_custom = GaussianProcessRegressor(\n kernel=custom_kernel,\n n_restarts_optimizer=15,\n normalize_y=True,\n random_state=42\n)\n\noptimizer_custom_gp = SpotOptim(\n fun=objective,\n bounds=[(-5, 5), (-5, 5)],\n surrogate=gp_custom,\n max_iter=10,\n n_initial=5,\n seed=42\n)\nresult = optimizer_custom_gp.optimize()\n```\n:::\n\n\n::: {#d0cabd35 .cell execution_count=8}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom sklearn.ensemble import RandomForestRegressor\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 8: Using Random Forest as surrogate\nrf_model = RandomForestRegressor(\n n_estimators=100,\n max_depth=10,\n random_state=42\n)\n\noptimizer_rf = SpotOptim(\n fun=objective,\n bounds=[(-5, 5), (-5, 5)],\n surrogate=rf_model,\n max_iter=10,\n n_initial=5,\n seed=42\n)\nresult = optimizer_rf.optimize()\n\n# Note: Random Forests don't provide uncertainty estimates,\n# so Expected Improvement (EI) may be less effective.\n# Consider using acquisition='y' for pure exploitation.\n```\n:::\n\n\n::: {#9e879121 .cell execution_count=9}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom sklearn.gaussian_process import GaussianProcessRegressor\nfrom sklearn.gaussian_process.kernels import Matern, RationalQuadratic, ConstantKernel, RBF\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 9: Comparing different kernels for Gaussian Process\n# Matern kernel with nu=1.5 (once differentiable)\nkernel_matern15 = ConstantKernel(1.0) * Matern(length_scale=1.0, nu=1.5)\ngp_matern15 = GaussianProcessRegressor(kernel=kernel_matern15, normalize_y=True)\n\n# Matern kernel with nu=2.5 (twice differentiable, DEFAULT)\nkernel_matern25 = ConstantKernel(1.0) * Matern(length_scale=1.0, nu=2.5)\ngp_matern25 = GaussianProcessRegressor(kernel=kernel_matern25, normalize_y=True)\n\n# RBF kernel (infinitely differentiable, smooth)\nkernel_rbf = ConstantKernel(1.0) * RBF(length_scale=1.0)\ngp_rbf = GaussianProcessRegressor(kernel=kernel_rbf, normalize_y=True)\n\n# Rational Quadratic kernel (mixture of RBF kernels)\nkernel_rq = ConstantKernel(1.0) * RationalQuadratic(length_scale=1.0, alpha=1.0)\ngp_rq = GaussianProcessRegressor(kernel=kernel_rq, normalize_y=True)\n\n# Use any of these as surrogate\noptimizer_rbf = SpotOptim(fun=objective, bounds=[(-5, 5), (-5, 5)],\n surrogate=gp_rbf, max_iter=10, n_initial=5)\nresult = optimizer_rbf.optimize()\n```\n:::\n\n\n## Methods\n\n| Name | Description |\n| --- | --- |\n| [aggregate_mean_var](#spotoptim.SpotOptim.SpotOptim.aggregate_mean_var) | Aggregate X and y values to compute mean and variance per group. |\n| [apply_ocba](#spotoptim.SpotOptim.SpotOptim.apply_ocba) | Apply Optimal Computing Budget Allocation for noisy functions. |\n| [apply_penalty_NA](#spotoptim.SpotOptim.SpotOptim.apply_penalty_NA) | Replace NaN and infinite values with penalty plus random noise. |\n| [check_size_initial_design](#spotoptim.SpotOptim.SpotOptim.check_size_initial_design) | Validate that initial design has sufficient points for surrogate fitting. |\n| [curate_initial_design](#spotoptim.SpotOptim.SpotOptim.curate_initial_design) | Remove duplicates and ensure sufficient unique points in initial design. |\n| [detect_var_type](#spotoptim.SpotOptim.SpotOptim.detect_var_type) | Auto-detect variable types based on factor mappings. |\n| [determine_termination](#spotoptim.SpotOptim.SpotOptim.determine_termination) | Determine termination reason for optimization. |\n| [evaluate_function](#spotoptim.SpotOptim.SpotOptim.evaluate_function) | Evaluate objective function at points X. |\n| [execute_optimization_run](#spotoptim.SpotOptim.SpotOptim.execute_optimization_run) | Entry point for a single sequential optimization run. |\n| [fit_scheduler](#spotoptim.SpotOptim.SpotOptim.fit_scheduler) | Fit surrogate model using appropriate data based on noise handling. |\n| [fit_select_best_cluster](#spotoptim.SpotOptim.SpotOptim.fit_select_best_cluster) | Selects all points from the cluster with the smallest mean y value. |\n| [fit_select_distant_points](#spotoptim.SpotOptim.SpotOptim.fit_select_distant_points) | Selects k points that are distant from each other using K-means clustering. |\n| [fit_selection_dispatcher](#spotoptim.SpotOptim.SpotOptim.fit_selection_dispatcher) | Dispatcher for selection methods. |\n| [fit_surrogate](#spotoptim.SpotOptim.SpotOptim.fit_surrogate) | Fit surrogate model to data. |\n| [gen_design_table](#spotoptim.SpotOptim.SpotOptim.gen_design_table) | Generate a table of the design or results. |\n| [generate_initial_design](#spotoptim.SpotOptim.SpotOptim.generate_initial_design) | Generate initial space-filling design using Latin Hypercube Sampling. |\n| [get_best_hyperparameters](#spotoptim.SpotOptim.SpotOptim.get_best_hyperparameters) | Get the best hyperparameter configuration found during optimization. |\n| [get_best_xy_initial_design](#spotoptim.SpotOptim.SpotOptim.get_best_xy_initial_design) | Determine and store the best point from initial design. |\n| [get_design_table](#spotoptim.SpotOptim.SpotOptim.get_design_table) | Get a table string showing the search space design before optimization. |\n| [get_experiment_filename](#spotoptim.SpotOptim.SpotOptim.get_experiment_filename) | Generate experiment filename with '_exp.pkl' suffix. |\n| [get_importance](#spotoptim.SpotOptim.SpotOptim.get_importance) | Calculate variable importance scores. |\n| [get_initial_design](#spotoptim.SpotOptim.SpotOptim.get_initial_design) | Generate or process initial design points. Ensures that design points are in |\n| [get_ocba](#spotoptim.SpotOptim.SpotOptim.get_ocba) | Optimal Computing Budget Allocation (OCBA). |\n| [get_ocba_X](#spotoptim.SpotOptim.SpotOptim.get_ocba_X) | Calculate OCBA allocation and repeat input array X. |\n| [get_pickle_safe_optimizer](#spotoptim.SpotOptim.SpotOptim.get_pickle_safe_optimizer) | Create a pickle-safe copy of the optimizer. |\n| [get_ranks](#spotoptim.SpotOptim.SpotOptim.get_ranks) | Returns ranks of numbers within input array x. |\n| [get_result_filename](#spotoptim.SpotOptim.SpotOptim.get_result_filename) | Generate result filename with '_res.pkl' suffix. |\n| [get_results_table](#spotoptim.SpotOptim.SpotOptim.get_results_table) | Get a comprehensive table string of optimization results. |\n| [get_shape](#spotoptim.SpotOptim.SpotOptim.get_shape) | Get the shape of the objective function output. |\n| [get_stars](#spotoptim.SpotOptim.SpotOptim.get_stars) | Converts a list of values to a list of stars. |\n| [get_success_rate](#spotoptim.SpotOptim.SpotOptim.get_success_rate) | Get the current success rate of the optimization process. |\n| [handle_default_var_trans](#spotoptim.SpotOptim.SpotOptim.handle_default_var_trans) | Handle default variable transformations. Does not perform any transformations, |\n| [init_storage](#spotoptim.SpotOptim.SpotOptim.init_storage) | Initialize storage for optimization. |\n| [init_surrogate](#spotoptim.SpotOptim.SpotOptim.init_surrogate) | Initialize or configure the surrogate model for optimization. Handles three surrogate configurations: |\n| [inverse_transform_X](#spotoptim.SpotOptim.SpotOptim.inverse_transform_X) | Transform parameter array from internal to original scale. |\n| [inverse_transform_value](#spotoptim.SpotOptim.SpotOptim.inverse_transform_value) | Apply inverse transformation to a single float value. |\n| [load_experiment](#spotoptim.SpotOptim.SpotOptim.load_experiment) | Load experiment configuration from a pickle file. |\n| [load_result](#spotoptim.SpotOptim.SpotOptim.load_result) | Load complete optimization results from a pickle file. |\n| [map_to_factor_values](#spotoptim.SpotOptim.SpotOptim.map_to_factor_values) | Map internal integer factor values back to string labels. |\n| [mo2so](#spotoptim.SpotOptim.SpotOptim.mo2so) | Convert multi-objective values to single-objective. |\n| [modify_bounds_based_on_var_type](#spotoptim.SpotOptim.SpotOptim.modify_bounds_based_on_var_type) | Modify bounds based on variable types. |\n| [optimize](#spotoptim.SpotOptim.SpotOptim.optimize) | Run the optimization process. The optimization terminates when either the total function evaluations reach |\n| [optimize_acquisition_func](#spotoptim.SpotOptim.SpotOptim.optimize_acquisition_func) | Optimize the acquisition function to find the next point to evaluate. |\n| [optimize_sequential_run](#spotoptim.SpotOptim.SpotOptim.optimize_sequential_run) | Perform a single sequential optimization run. |\n| [plot_importance](#spotoptim.SpotOptim.SpotOptim.plot_importance) | Plot variable importance. |\n| [plot_important_hyperparameter_contour](#spotoptim.SpotOptim.SpotOptim.plot_important_hyperparameter_contour) | Plot surrogate contours using spotoptim.plot.visualization.plot_important_hyperparameter_contour. |\n| [plot_parameter_scatter](#spotoptim.SpotOptim.SpotOptim.plot_parameter_scatter) | Plot parameter distributions showing relationship between each parameter and objective. |\n| [plot_progress](#spotoptim.SpotOptim.SpotOptim.plot_progress) | Plot optimization progress using spotoptim.plot.visualization.plot_progress. |\n| [plot_surrogate](#spotoptim.SpotOptim.SpotOptim.plot_surrogate) | Plot the surrogate model for two dimensions. |\n| [print_best](#spotoptim.SpotOptim.SpotOptim.print_best) | Print the best solution found during optimization. |\n| [print_results](#spotoptim.SpotOptim.SpotOptim.print_results) | Alias for print(get_results_table()) for compatibility. |\n| [process_factor_bounds](#spotoptim.SpotOptim.SpotOptim.process_factor_bounds) | Process `bounds` to handle factor variables. |\n| [reinitialize_components](#spotoptim.SpotOptim.SpotOptim.reinitialize_components) | Reinitialize components that were excluded during pickling. |\n| [remove_nan](#spotoptim.SpotOptim.SpotOptim.remove_nan) | Remove rows where y contains NaN or inf values. |\n| [repair_natural_X](#spotoptim.SpotOptim.SpotOptim.repair_natural_X) | Enforce integrality and declared bounds in natural (original) space. |\n| [repair_non_numeric](#spotoptim.SpotOptim.SpotOptim.repair_non_numeric) | Round non-numeric values to integers based on variable type. |\n| [rm_initial_design_NA_values](#spotoptim.SpotOptim.SpotOptim.rm_initial_design_NA_values) | Remove NaN/inf values from initial design evaluations. |\n| [save_experiment](#spotoptim.SpotOptim.SpotOptim.save_experiment) | Save experiment configuration to a pickle file. |\n| [save_result](#spotoptim.SpotOptim.SpotOptim.save_result) | Save complete optimization results to a pickle file. |\n| [select_new](#spotoptim.SpotOptim.SpotOptim.select_new) | Select rows from A that are not in X. |\n| [sensitivity_spearman](#spotoptim.SpotOptim.SpotOptim.sensitivity_spearman) | Compute and print Spearman correlation between parameters and objective values. |\n| [set_seed](#spotoptim.SpotOptim.SpotOptim.set_seed) | Set global random seeds for reproducibility. |\n| [setup_dimension_reduction](#spotoptim.SpotOptim.SpotOptim.setup_dimension_reduction) | Set up dimension reduction by identifying fixed dimensions. |\n| [store_mo](#spotoptim.SpotOptim.SpotOptim.store_mo) | Store multi-objective values in self.y_mo. |\n| [suggest_next_infill_point](#spotoptim.SpotOptim.SpotOptim.suggest_next_infill_point) | Suggest next point to evaluate (dispatcher). |\n| [to_all_dim](#spotoptim.SpotOptim.SpotOptim.to_all_dim) | Expand reduced-dimensional points to full-dimensional representation. |\n| [to_red_dim](#spotoptim.SpotOptim.SpotOptim.to_red_dim) | Reduce full-dimensional points to optimization space. |\n| [transform_X](#spotoptim.SpotOptim.SpotOptim.transform_X) | Transform parameter array from original (natural) to internal scale. |\n| [transform_bounds](#spotoptim.SpotOptim.SpotOptim.transform_bounds) | Transform bounds from original to internal scale. |\n| [transform_value](#spotoptim.SpotOptim.SpotOptim.transform_value) | Apply transformation to a single float value. |\n| [update_repeats_infill_points](#spotoptim.SpotOptim.SpotOptim.update_repeats_infill_points) | Repeat infill point for noisy function evaluation. Used in the sequential_loop. |\n| [update_stats](#spotoptim.SpotOptim.SpotOptim.update_stats) | Update optimization statistics. |\n| [update_storage](#spotoptim.SpotOptim.SpotOptim.update_storage) | Update storage (`X_`, `y_`) with new evaluation points. |\n| [update_success_rate](#spotoptim.SpotOptim.SpotOptim.update_success_rate) | Update the rolling success rate of the optimization process. |\n| [validate_x0](#spotoptim.SpotOptim.SpotOptim.validate_x0) | Validate and process starting point x0. Called in `__init__` and `optimize`. |\n\n### aggregate_mean_var { #spotoptim.SpotOptim.SpotOptim.aggregate_mean_var }\n\n```python\nSpotOptim.SpotOptim.aggregate_mean_var(X, y)\n```\n\nAggregate X and y values to compute mean and variance per group.\nFor repeated evaluations at the same design point, this method computes\nthe mean function value and variance (using population variance, ddof=0).\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Design points, shape (n_samples, n_features). | _required_ |\n| y | [ndarray](`ndarray`) | Function values, shape (n_samples,). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| tuple | [Tuple](`typing.Tuple`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`)\\] | A tuple containing: * X_agg (ndarray): Unique design points, shape (n_groups, n_features) * y_mean (ndarray): Mean y values per group, shape (n_groups,) * y_var (ndarray): Variance of y values per group, shape (n_groups,) |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#869ca22e .cell execution_count=10}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n repeats_initial=2)\nX = np.array([[1, 2], [3, 4], [1, 2]])\ny = np.array([1, 2, 3])\nX_agg, y_mean, y_var = opt.aggregate_mean_var(X, y)\nprint(X_agg.shape)\nprint(y_mean)\nprint(y_var)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(2, 2)\n[2. 2.]\n[1. 0.]\n```\n:::\n:::\n\n\n### apply_ocba { #spotoptim.SpotOptim.SpotOptim.apply_ocba }\n\n```python\nSpotOptim.SpotOptim.apply_ocba()\n```\n\nApply Optimal Computing Budget Allocation for noisy functions.\n\n### apply_penalty_NA { #spotoptim.SpotOptim.SpotOptim.apply_penalty_NA }\n\n```python\nSpotOptim.SpotOptim.apply_penalty_NA(\n y,\n y_history=None,\n penalty_value=None,\n sd=0.1,\n)\n```\n\nReplace NaN and infinite values with penalty plus random noise.\nUsed in the optimize() method after function evaluations.\nThis method follows the approach from spotpython.utils.repair.apply_penalty_NA,\nreplacing NaN/inf values with a penalty value plus random noise to avoid\nidentical penalty values.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|---------------|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|\n| y | [ndarray](`ndarray`) | Array of objective function values to be repaired. | _required_ |\n| y_history | [ndarray](`ndarray`) | Historical objective function values used for computing penalty statistics. If None, uses y itself. Default is None. | `None` |\n| penalty_value | [float](`float`) | Value to replace NaN/inf with. If None, computes penalty as: max(finite_y_history) + 3 * std(finite_y_history). If all values are NaN/inf or only one finite value exists, falls back to self.penalty_val. Default is None. | `None` |\n| sd | [float](`float`) | Standard deviation for normal distributed random noise added to penalty. Default is 0.1. | `0.1` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Array with NaN/inf replaced by penalty_value + random noise (normal distributed with mean 0 and standard deviation sd). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#8cd2b351 .cell execution_count=11}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1), bounds=[(-5, 5)])\ny_hist = np.array([1.0, 2.0, 3.0, 5.0])\ny_new = np.array([4.0, np.nan, np.inf])\ny_clean = opt.apply_penalty_NA(y_new, y_history=y_hist)\nprint(f\"np.all(np.isfinite(y_clean)): {np.all(np.isfinite(y_clean))}\")\nprint(f\"y_clean: {y_clean}\")\n# NaN/inf replaced with worst value from history + 3*std + noise\nprint(f\"y_clean[1] > 5.0: {y_clean[1] > 5.0}\") # Should be larger than max finite value in history\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nnp.all(np.isfinite(y_clean)): True\ny_clean: [4. 9.99213659 9.97045717]\ny_clean[1] > 5.0: True\n```\n:::\n:::\n\n\n### check_size_initial_design { #spotoptim.SpotOptim.SpotOptim.check_size_initial_design }\n\n```python\nSpotOptim.SpotOptim.check_size_initial_design(y0, n_evaluated)\n```\n\nValidate that initial design has sufficient points for surrogate fitting.\n\nChecks if the number of valid initial design points meets the minimum\nrequirement for fitting a surrogate model. The minimum required is the\nsmaller of:\n * (a) typical minimum for surrogate fitting (3 for multi-dimensional, 2 for 1D), or\n * (b) what the user requested (`n_initial`).\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-------------|----------------------|-------------------------------------------------------------------------------|------------|\n| y0 | [ndarray](`ndarray`) | Function values at initial design points (after filtering), shape (n_valid,). | _required_ |\n| n_evaluated | [int](`int`) | Original number of points evaluated before filtering. | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|------------------------------------------------------------------|\n| | [ValueError](`ValueError`) | If the number of valid points is less than the minimum required. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#5db0c341 .cell execution_count=12}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=10\n)\n# Sufficient points - no error\ny0 = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\nopt.check_size_initial_design(y0, n_evaluated=10)\n\n# Insufficient points - raises ValueError\ny0_small = np.array([1.0])\ntry:\n opt.check_size_initial_design(y0_small, n_evaluated=10)\nexcept ValueError as e:\n print(f\"Error: {e}\")\n\n# With verbose output\nopt_verbose = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=10,\n verbose=True\n)\ny0_reduced = np.array([1.0, 2.0, 3.0]) # Less than n_initial but valid\nopt_verbose.check_size_initial_design(y0_reduced, n_evaluated=10)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nError: Insufficient valid initial design points: only 1 finite value(s) out of 10 evaluated. Need at least 3 points to fit surrogate model. Please check your objective function or increase n_initial.\nTensorBoard logging disabled\nNote: Initial design size (3) is smaller than requested (10) due to NaN/inf values\n```\n:::\n:::\n\n\n### curate_initial_design { #spotoptim.SpotOptim.SpotOptim.curate_initial_design }\n\n```python\nSpotOptim.SpotOptim.curate_initial_design(X0)\n```\n\nRemove duplicates and ensure sufficient unique points in initial design.\n\nThis method handles deduplication that can occur after rounding integer/factor\nvariables. If duplicates are found, it generates additional points to reach\nthe target n_initial unique points. Also handles repeating points when\nrepeats_initial > 1.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-------------------------------------------------------------------------|------------|\n| X0 | [ndarray](`ndarray`) | Initial design points in internal scale, shape (n_samples, n_features). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Curated initial design with duplicates removed and repeated if necessary, shape (n_unique * repeats_initial, n_features). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#396570c9 .cell execution_count=13}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=10,\n var_type=['int', 'int'] # Integer variables may cause duplicates\n)\nX0 = opt.get_initial_design()\nX0_curated = opt.curate_initial_design(X0)\nX0_curated.shape[0] == 10 # Should have n_initial unique points\n```\n\n::: {.cell-output .cell-output-display execution_count=13}\n```\nTrue\n```\n:::\n:::\n\n\n::: {#58cba70c .cell execution_count=14}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n# With repeats\nopt_repeat = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n repeats_initial=3\n)\nX0 = opt_repeat.get_initial_design()\nX0_curated = opt_repeat.curate_initial_design(X0)\nX0_curated.shape[0] == 15 # 5 unique points * 3 repeats\n```\n\n::: {.cell-output .cell-output-display execution_count=14}\n```\nTrue\n```\n:::\n:::\n\n\n### detect_var_type { #spotoptim.SpotOptim.SpotOptim.detect_var_type }\n\n```python\nSpotOptim.SpotOptim.detect_var_type()\n```\n\nAuto-detect variable types based on factor mappings.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|----------------|-------------------------------------------------------------------------------------------------------------------------------------------|\n| list | [list](`list`) | List of variable types ('factor' or 'float') for each dimension. Dimensions with factor mappings are assigned 'factor', others 'float'. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#de10719a .cell execution_count=15}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\n\n# Define a simple objective mapping names to values for demonstration\ndef objective(X):\n # X has shape (n_samples, n_dimensions)\n return X[:, 0] + X[:, 1]\n\n# The first dimension has factor levels ('red', 'green', 'blue')\n# The second dimension is continuous bounds (0, 10)\nspot = SpotOptim(fun=objective, bounds=[('red', 'green', 'blue'), (0, 10)])\nprint(spot.detect_var_type())\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n['factor', 'float']\n```\n:::\n:::\n\n\n### determine_termination { #spotoptim.SpotOptim.SpotOptim.determine_termination }\n\n```python\nSpotOptim.SpotOptim.determine_termination(timeout_start)\n```\n\nDetermine termination reason for optimization.\nChecks the termination conditions and returns an appropriate message\nindicating why the optimization stopped. Three possible termination\nconditions are checked in order of priority:\n 1. Maximum number of evaluations reached\n 2. Maximum time limit exceeded\n 3. Successful completion (neither limit reached)\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|---------------|------------------|------------------------------------------------|------------|\n| timeout_start | [float](`float`) | Start time of optimization (from time.time()). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------------|--------------------------------------------|\n| str | [str](`str`) | Message describing the termination reason. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#7147d096 .cell execution_count=16}\n``` {.python .cell-code}\nimport numpy as np\nimport time\nfrom spotoptim import SpotOptim\nopt = SpotOptim(\n fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n max_time=10.0\n)\n# Case 1: Maximum evaluations reached\nopt.y_ = np.zeros(20) # Simulate 20 evaluations\nstart_time = time.time()\nmsg = opt.determine_termination(start_time)\nprint(msg)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nOptimization terminated: maximum evaluations (10) reached\n```\n:::\n:::\n\n\n::: {#c869e676 .cell execution_count=17}\n``` {.python .cell-code}\n# Case 2: Time limit exceeded\nimport numpy as np\nimport time\nfrom spotoptim import SpotOptim\nopt.y_ = np.zeros(10) # Only 10 evaluations\nstart_time = time.time() - 700 # Simulate 11.67 minutes elapsed\nmsg = opt.determine_termination(start_time)\nprint(msg)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nOptimization terminated: maximum evaluations (10) reached\n```\n:::\n:::\n\n\n::: {#c2b6f85a .cell execution_count=18}\n``` {.python .cell-code}\n# Case 3: Successful completion\nimport numpy as np\nimport time\nfrom spotoptim import SpotOptim\nopt.y_ = np.zeros(10) # Under max_iter\nstart_time = time.time() # Just started\nmsg = opt.determine_termination(start_time)\nprint(msg)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nOptimization terminated: maximum evaluations (10) reached\n```\n:::\n:::\n\n\n### evaluate_function { #spotoptim.SpotOptim.SpotOptim.evaluate_function }\n\n```python\nSpotOptim.SpotOptim.evaluate_function(X)\n```\n\nEvaluate objective function at points X.\nUsed in the optimize() method to evaluate the objective function.\n\nInput Space: `X` is expected in Transformed and Mapped Space (Internal scale, Reduced dimensions).\nProcess as follows:\n 1. Expands `X` to Transformed Space (Full dimensions) if dimension reduction is active.\n 2. Inverse transforms `X` to Natural Space (Original scale).\n 3. Evaluates the user function with points in Natural Space.\n\nIf dimension reduction is active, expands `X` to full dimensions before evaluation.\nSupports both single-objective and multi-objective functions. For multi-objective\nfunctions, converts to single-objective using `mo2so` method.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|--------------------------------------------------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Points to evaluate in Transformed and Mapped Space, shape (n_samples, n_reduced_features). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|--------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Function values, shape (n_samples,). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#5b32bd11 .cell execution_count=19}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n# Single-objective function\nopt_so = SpotOptim(\n fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5\n)\nX = np.array([[1.0, 2.0], [3.0, 4.0]])\ny = opt_so.evaluate_function(X)\nprint(f\"Single-objective output: {y}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSingle-objective output: [ 5. 25.]\n```\n:::\n:::\n\n\n::: {#974f9fa2 .cell execution_count=20}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n# Multi-objective function (default: use first objective)\nopt_mo = SpotOptim(\n fun=lambda X: np.column_stack([\n np.sum(X**2, axis=1),\n np.sum((X-1)**2, axis=1)\n ]),\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5\n)\ny_mo = opt_mo.evaluate_function(X)\nprint(f\"Multi-objective output (first obj): {y_mo}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nMulti-objective output (first obj): [ 5. 25.]\n```\n:::\n:::\n\n\n### execute_optimization_run { #spotoptim.SpotOptim.SpotOptim.execute_optimization_run }\n\n```python\nSpotOptim.SpotOptim.execute_optimization_run(\n timeout_start,\n X0=None,\n y0_known=None,\n max_iter_override=None,\n)\n```\n\nEntry point for a single sequential optimization run.\nDelegates to optimize_sequential_run with the provided arguments.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-------------------|---------------------------------------------------------------------------|------------------------------------------------------------------------|------------|\n| timeout_start | [float](`float`) | Start time for timeout. | _required_ |\n| X0 | [Optional](`typing.Optional`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`)\\] | Initial design points in Natural Space, shape (n_initial, n_features). | `None` |\n| y0_known | [Optional](`typing.Optional`)\\[[float](`float`)\\] | Known best value for initial design. | `None` |\n| max_iter_override | [Optional](`typing.Optional`)\\[[int](`int`)\\] | Override for maximum number of iterations. | `None` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|\n| | [Tuple](`typing.Tuple`)\\[[str](`str`), [OptimizeResult](`scipy.optimize.OptimizeResult`)\\] | Tuple[str, OptimizeResult]: Tuple containing status and optimization result. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#a9f18325 .cell execution_count=21}\n``` {.python .cell-code}\nimport time\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n max_iter=10,\n seed=0,\n verbose=True\n)\nstatus, result = opt.execute_optimization_run(timeout_start=time.time())\nprint(status)\nprint(result.message.splitlines()[0])\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 8.463203\nIter 1 | Best: 8.463203 | Curr: 18.224245 | Rate: 0.00 | Evals: 60.0%\nIter 2 | Best: 8.412459 | Rate: 0.50 | Evals: 70.0%\nIter 3 | Best: 5.623369 | Rate: 0.67 | Evals: 80.0%\nIter 4 | Best: 2.902974 | Rate: 0.75 | Evals: 90.0%\nIter 5 | Best: 0.022050 | Rate: 0.80 | Evals: 100.0%\nFINISHED\nOptimization terminated: maximum evaluations (10) reached\n```\n:::\n:::\n\n\n### fit_scheduler { #spotoptim.SpotOptim.SpotOptim.fit_scheduler }\n\n```python\nSpotOptim.SpotOptim.fit_scheduler()\n```\n\nFit surrogate model using appropriate data based on noise handling.\nThis method selects the appropriate training data for surrogate fitting:\n * For noisy functions (repeats_surrogate > 1): Uses mean_X and mean_y (aggregated values)\n * For deterministic functions: Uses X_ and y_ (all evaluated points)\nThe data is transformed to internal scale before fitting the surrogate.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n```python\n>>> import numpy as np\n>>> from spotoptim import SpotOptim\n>>> from sklearn.gaussian_process import GaussianProcessRegressor\n>>> # Deterministic function\n>>> def sphere(X):\n... X = np.atleast_2d(X)\n... return np.sum(X**2, axis=1)\n>>> opt = SpotOptim(\n... fun=sphere,\n... bounds=[(-5, 5), (-5, 5)],\n... surrogate=GaussianProcessRegressor(),\n... n_initial=5\n... )\n>>> # Simulate optimization state\n>>> opt.X_ = np.array([[1, 2], [0, 0], [2, 1]])\n>>> opt.y_ = np.array([5.0, 0.0, 5.0])\n>>> opt.fit_scheduler()\n>>> # Surrogate fitted with X_ and y_\n>>>\n>>> # Noisy function\n>>> def sphere(X):\n... X = np.atleast_2d(X)\n... return np.sum(X**2, axis=1)\n>>> opt_noise = SpotOptim(\n... fun=sphere,\n... bounds=[(-5, 5), (-5, 5)],\n... surrogate=GaussianProcessRegressor(),\n... n_initial=5,\n... repeats_initial=3,\n... )\n>>> # Simulate noisy optimization state\n>>> opt_noise.mean_X = np.array([[1, 2], [0, 0]])\n>>> opt_noise.mean_y = np.array([5.0, 0.0])\n>>> opt_noise.fit_scheduler()\n>>> # Surrogate fitted with mean_X and mean_y\n```\n\n### fit_select_best_cluster { #spotoptim.SpotOptim.SpotOptim.fit_select_best_cluster }\n\n```python\nSpotOptim.SpotOptim.fit_select_best_cluster(X, y, k)\n```\n\nSelects all points from the cluster with the smallest mean y value.\nThis method performs K-means clustering and selects all points from the\ncluster whose center corresponds to the best (smallest) mean objective\nfunction value.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Design points, shape (n_samples, n_features). | _required_ |\n| y | [ndarray](`ndarray`) | Function values at X, shape (n_samples,). | _required_ |\n| k | [int](`int`) | Number of clusters. | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|---------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| tuple | [Tuple](`typing.Tuple`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`)\\] | A tuple containing: * selected_X (ndarray): Selected design points from best cluster, shape (m, n_features). * selected_y (ndarray): Function values at selected points, shape (m,). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#c2a13373 .cell execution_count=22}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_surrogate_points=5,\n selection_method='best')\nX = np.random.rand(100, 2)\ny = np.random.rand(100)\nX_sel, y_sel = opt.fit_select_best_cluster(X, y, 5)\nprint(f\"X_sel.shape: {X_sel.shape}\")\nprint(f\"y_sel.shape: {y_sel.shape}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nX_sel.shape: (25, 2)\ny_sel.shape: (25,)\n```\n:::\n:::\n\n\n### fit_select_distant_points { #spotoptim.SpotOptim.SpotOptim.fit_select_distant_points }\n\n```python\nSpotOptim.SpotOptim.fit_select_distant_points(X, y, k)\n```\n\nSelects k points that are distant from each other using K-means clustering.\nThis method performs K-means clustering to find k clusters, then selects\nthe point closest to each cluster center. This ensures a space-filling\nsubset of points for surrogate model training.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Design points, shape (n_samples, n_features). | _required_ |\n| y | [ndarray](`ndarray`) | Function values at X, shape (n_samples,). | _required_ |\n| k | [int](`int`) | Number of points to select. | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|---------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| tuple | [Tuple](`typing.Tuple`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`)\\] | A tuple containing: * selected_X (ndarray): Selected design points, shape (k, n_features). * selected_y (ndarray): Function values at selected points, shape (k,). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#0434e10d .cell execution_count=23}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_surrogate_points=5)\nX = np.random.rand(100, 2)\ny = np.random.rand(100)\nX_sel, y_sel = opt.fit_select_distant_points(X, y, 5)\nprint(X_sel.shape)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(5, 2)\n```\n:::\n:::\n\n\n### fit_selection_dispatcher { #spotoptim.SpotOptim.SpotOptim.fit_selection_dispatcher }\n\n```python\nSpotOptim.SpotOptim.fit_selection_dispatcher(X, y)\n```\n\nDispatcher for selection methods.\nDepending on the value of `self.selection_method`, this method calls\nthe appropriate selection function to choose a subset of points for\nsurrogate model training when the total number of points exceeds\n`self.max_surrogate_points`.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Design points, shape (n_samples, n_features). | _required_ |\n| y | [ndarray](`ndarray`) | Function values at X, shape (n_samples,). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|---------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------|\n| tuple | [Tuple](`typing.Tuple`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`)\\] | A tuple containing: * selected_X (ndarray): Selected design points. * selected_y (ndarray): Function values at selected points. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#4930a0f8 .cell execution_count=24}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_surrogate_points=5)\nX = np.random.rand(100, 2)\ny = np.random.rand(100)\nX_sel, y_sel = opt.fit_selection_dispatcher(X, y)\nprint(X_sel.shape[0] <= 5)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTrue\n```\n:::\n:::\n\n\n### fit_surrogate { #spotoptim.SpotOptim.SpotOptim.fit_surrogate }\n\n```python\nSpotOptim.SpotOptim.fit_surrogate(X, y)\n```\n\nFit surrogate model to data.\nUsed by fit_scheduler() to fit the surrogate model.\nIf the number of points exceeds `self.max_surrogate_points`,\na subset of points is selected using the selection dispatcher.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Design points, shape (n_samples, n_features). | _required_ |\n| y | [ndarray](`ndarray`) | Function values at X, shape (n_samples,). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n```python\n>>> import numpy as np\n>>> from spotoptim import SpotOptim\n>>> from sklearn.gaussian_process import GaussianProcessRegressor\n>>> def sphere(X):\n... X = np.atleast_2d(X)\n... return np.sum(X**2, axis=1)\n>>> opt = SpotOptim(fun=sphere,\n... bounds=[(-5, 5), (-5, 5)],\n... max_surrogate_points=10,\n... surrogate=GaussianProcessRegressor())\n>>> X = np.random.rand(50, 2)\n>>> y = np.random.rand(50)\n>>> opt.fit_surrogate(X, y)\n>>> # Surrogate is now fitted\n```\n\n### gen_design_table { #spotoptim.SpotOptim.SpotOptim.gen_design_table }\n\n```python\nSpotOptim.SpotOptim.gen_design_table(precision=4, tablefmt='github')\n```\n\nGenerate a table of the design or results.\nIf optimization has been run (results available), returns the results table.\nOtherwise, returns the design table (search space configuration).\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------|--------------|-----------------------------------------------------------|------------|\n| tablefmt | [str](`str`) | Table format. Defaults to 'github'. | `'github'` |\n| precision | [int](`int`) | Number of decimal places for float values. Defaults to 4. | `4` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------------|-------------------------|\n| str | [str](`str`) | Formatted table string. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#13cc3f91 .cell execution_count=25}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-10, 10), (0, 1)],\n var_name=[\"x1\", \"x2\", \"x3\"],\n var_type=[\"float\", \"int\", \"float\"],\n max_iter=10,\n n_initial=5\n)\ntable = opt.gen_design_table()\nprint(table)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n| name | type | lower | upper | default | transform |\n|--------|--------|---------|---------|-----------|-------------|\n| x1 | float | -5 | 5 | 0 | - |\n| x2 | int | -10 | 10 | 0 | - |\n| x3 | float | 0 | 1 | 0.5 | - |\n```\n:::\n:::\n\n\n### generate_initial_design { #spotoptim.SpotOptim.SpotOptim.generate_initial_design }\n\n```python\nSpotOptim.SpotOptim.generate_initial_design()\n```\n\nGenerate initial space-filling design using Latin Hypercube Sampling.\nUsed in the optimize() method to create the initial set of design points.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|-------------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Initial design points, shape (n_initial, n_features). Points are in the intervals defined by `self.bounds`. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#3fccaf4d .cell execution_count=26}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=3,\n var_type=['float', 'int'],\n var_trans=['log10', None])\nX0 = opt.generate_initial_design()\nprint(X0.shape)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(3, 2)\n```\n:::\n:::\n\n\n### get_best_hyperparameters { #spotoptim.SpotOptim.SpotOptim.get_best_hyperparameters }\n\n```python\nSpotOptim.SpotOptim.get_best_hyperparameters(as_dict=True)\n```\n\nGet the best hyperparameter configuration found during optimization.\nIf noise handling is active (repeats_initial > 1 or OCBA), this returns the parameter\nconfiguration associated with the best *mean* objective value. Otherwise, it returns\nthe configuration associated with the absolute best observed value.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|---------|----------------|---------------------------------------------------------------------------------------------------------------------------------|-----------|\n| as_dict | [bool](`bool`) | If True, returns a dictionary mapping parameter names to their values. If False, returns the raw numpy array. Defaults to True. | `True` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|---------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|\n| | [Union](`typing.Union`)\\[[Dict](`typing.Dict`)\\[[str](`str`), [Any](`typing.Any`)\\], [np](`numpy`).[ndarray](`numpy.ndarray`), None\\] | Union[Dict[str, Any], np.ndarray, None]: The best hyperparameter configuration. Returns None if optimization hasn't started (no data). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#fcf39f9b .cell execution_count=27}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere,\n bounds=[(-5, 5), (0, 10)],\n n_initial=5,\n var_name=[\"x\", \"y\"],\n verbose=True)\nopt.optimize()\nbest_params = opt.get_best_hyperparameters()\nprint(best_params['x']) # Should be close to 0\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 21.001919\nIter 1 | Best: 10.937353 | Rate: 1.00 | Evals: 30.0%\nIter 2 | Best: 3.690681 | Rate: 1.00 | Evals: 35.0%\nIter 3 | Best: 0.171879 | Rate: 1.00 | Evals: 40.0%\nIter 4 | Best: 0.112225 | Rate: 1.00 | Evals: 45.0%\nIter 5 | Best: 0.000182 | Rate: 1.00 | Evals: 50.0%\nIter 6 | Best: 0.000001 | Rate: 1.00 | Evals: 55.0%\nIter 7 | Best: 0.000000 | Rate: 1.00 | Evals: 60.0%\nIter 8 | Best: 0.000000 | Rate: 1.00 | Evals: 65.0%\nIter 9 | Best: 0.000000 | Rate: 1.00 | Evals: 70.0%\nIter 10 | Best: 0.000000 | Rate: 1.00 | Evals: 75.0%\nIter 11 | Best: 0.000000 | Rate: 1.00 | Evals: 80.0%\nIter 12 | Best: 0.000000 | Rate: 1.00 | Evals: 85.0%\nIter 13 | Best: 0.000000 | Curr: 0.000000 | Rate: 0.92 | Evals: 90.0%\nIter 14 | Best: 0.000000 | Curr: 0.000000 | Rate: 0.86 | Evals: 95.0%\nIter 15 | Best: 0.000000 | Rate: 0.87 | Evals: 100.0%\n0.0003481606136886121\n```\n:::\n:::\n\n\n### get_best_xy_initial_design { #spotoptim.SpotOptim.SpotOptim.get_best_xy_initial_design }\n\n```python\nSpotOptim.SpotOptim.get_best_xy_initial_design()\n```\n\nDetermine and store the best point from initial design.\nFinds the best (minimum) function value in the initial design,\nstores the corresponding point and value in instance attributes,\nand optionally prints the results if verbose mode is enabled.\nFor noisy functions, also reports the mean best value.\n\n#### Note {.doc-section .doc-section-note}\n\nThis method assumes self.X_ and self.y_ have been initialized\nwith the initial design evaluations.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#86a8c68b .cell execution_count=28}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n verbose=True\n)\n# Simulate initial design (normally done in optimize())\nopt.X_ = np.array([[1, 2], [0, 0], [2, 1]])\nopt.y_ = np.array([5.0, 0.0, 5.0])\nopt.get_best_xy_initial_design()\nprint(f\"Best x: {opt.best_x_}\")\nprint(f\"Best y: {opt.best_y_}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 0.000000\nBest x: [0 0]\nBest y: 0.0\n```\n:::\n:::\n\n\n::: {#4a2675d6 .cell execution_count=29}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import noisy_sphere\n# With noisy function\nopt_noise = SpotOptim(\n fun=noisy_sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n repeats_surrogate=2,\n verbose=True\n)\nopt_noise.X_ = np.array([[1, 2], [0, 0], [2, 1]])\nopt_noise.y_ = np.array([5.0, 0.0, 5.0])\nopt_noise.min_mean_y = 0.5 # Simulated mean best\nopt_noise.get_best_xy_initial_design()\nprint(f\"Best x: {opt_noise.best_x_}\")\nprint(f\"Best y: {opt_noise.best_y_}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 0.000000, mean best: f(x) = 0.500000\nBest x: [0 0]\nBest y: 0.0\n```\n:::\n:::\n\n\n### get_design_table { #spotoptim.SpotOptim.SpotOptim.get_design_table }\n\n```python\nSpotOptim.SpotOptim.get_design_table(tablefmt='github', precision=4)\n```\n\nGet a table string showing the search space design before optimization.\nThis method generates a table displaying the variable names, types, bounds,\nand defaults without requiring an optimization run. Useful for inspecting\nand documenting the search space configuration.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------|--------------|-----------------------------------------------------------|------------|\n| tablefmt | [str](`str`) | Table format for tabulate library. Defaults to 'github'. | `'github'` |\n| precision | [int](`int`) | Number of decimal places for float values. Defaults to 4. | `4` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------------|-------------------------|\n| str | [str](`str`) | Formatted table string. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#5b5adedf .cell execution_count=30}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-10, 10), (0, 1)],\n var_name=[\"x1\", \"x2\", \"x3\"],\n var_type=[\"float\", \"int\", \"float\"],\n max_iter=10,\n n_initial=5\n)\ntable = opt.get_design_table()\nprint(table)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n| name | type | lower | upper | default | transform |\n|--------|--------|---------|---------|-----------|-------------|\n| x1 | float | -5 | 5 | 0 | - |\n| x2 | int | -10 | 10 | 0 | - |\n| x3 | float | 0 | 1 | 0.5 | - |\n```\n:::\n:::\n\n\n### get_experiment_filename { #spotoptim.SpotOptim.SpotOptim.get_experiment_filename }\n\n```python\nSpotOptim.SpotOptim.get_experiment_filename(prefix)\n```\n\nGenerate experiment filename with '_exp.pkl' suffix.\n\n### get_importance { #spotoptim.SpotOptim.SpotOptim.get_importance }\n\n```python\nSpotOptim.SpotOptim.get_importance()\n```\n\nCalculate variable importance scores.\nImportance is computed as the normalized sensitivity of each parameter\nbased on the variation in objective values across the evaluated points.\nHigher scores indicate parameters that have more influence on the objective.\nThe importance is calculated as:\n 1. For each dimension, compute the correlation between parameter values\n and objective values\n 2. Normalize to percentage scale (0-100)\n 3. Higher values indicate more important parameters\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|-------------------------------------------|------------------------------------------------------------------|\n| | [List](`typing.List`)\\[[float](`float`)\\] | List[float]: Importance scores for each dimension (0-100 scale). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#40ebff5d .cell execution_count=31}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef test_func(X):\n # x0 has strong effect, x1 has weak effect\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=10,\n n_initial=5,\n seed=42\n)\nresult = opt.optimize()\nimportance = opt.get_importance()\nprint(f\"x0 importance: {importance[0]:.2f}\")\nprint(f\"x1 importance: {importance[1]:.2f}\")\n\n# Use table to display importance\ntable = opt.get_results_table(show_importance=True)\nprint(table)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nx0 importance: 73.24\nx1 importance: 26.76\n| name | type | default | lower | upper | tuned | transform | importance | stars |\n|--------|--------|-----------|---------|---------|---------|-------------|--------------|---------|\n| x0 | float | 0 | -5 | 5 | 0.1701 | - | 73.24 | * |\n| x1 | float | 0 | -5 | 5 | 1.9552 | - | 26.76 | . |\n\nInterpretation: ***: >99%, **: >75%, *: >50%, .: >10%\n```\n:::\n:::\n\n\n### get_initial_design { #spotoptim.SpotOptim.SpotOptim.get_initial_design }\n\n```python\nSpotOptim.SpotOptim.get_initial_design(X0=None)\n```\n\nGenerate or process initial design points. Ensures that design points are in\ninternal (transformed and reduced) scale.\nCalls `generate_initial_design()` if `X0` is None, otherwise processes user-provided `X0`.\nHandles three scenarios:\n * `X0` is None: Generate space-filling design using LHS\n * `X0` is None but starting point(s) `x0` is provided: Generate LHS and include `x0` as first point(s)\n * `X0` is provided: Transform and prepare user-provided initial design\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|-----------|\n| X0 | [ndarray](`ndarray`) | User-provided initial design points in original scale, shape (n_initial, n_features). If None, generates space-filling design. Defaults to None. | `None` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|-----------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Initial design points in internal (transformed and reduced) scale, shape (n_initial, n_features_reduced). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#1842b498 .cell execution_count=32}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nfrom spotoptim.plot.visualization import plot_design_points\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=10\n)\n# Generate default LHS design\nX0 = opt.get_initial_design()\nprint(X0.shape)\nplot_design_points(X0)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(10, 2)\n```\n:::\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim.SpotOptim_files/figure-html/cell-33-output-2.png){width=565 height=469}\n:::\n\n::: {.cell-output .cell-output-display execution_count=32}\n![](SpotOptim.SpotOptim_files/figure-html/cell-33-output-3.png){width=565 height=469}\n:::\n:::\n\n\n::: {#249d5df2 .cell execution_count=33}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nfrom spotoptim.plot.visualization import plot_design_points\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=10,\n x0=np.array([0, 0]) # Starting point to include in initial design\n)\nX0 = opt.get_initial_design()\nprint(X0.shape)\nplot_design_points(X0)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(10, 2)\n```\n:::\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim.SpotOptim_files/figure-html/cell-34-output-2.png){width=565 height=469}\n:::\n\n::: {.cell-output .cell-output-display execution_count=33}\n![](SpotOptim.SpotOptim_files/figure-html/cell-34-output-3.png){width=565 height=469}\n:::\n:::\n\n\n### get_ocba { #spotoptim.SpotOptim.SpotOptim.get_ocba }\n\n```python\nSpotOptim.SpotOptim.get_ocba(means, vars, delta, verbose=False)\n```\n\nOptimal Computing Budget Allocation (OCBA).\n\n### get_ocba_X { #spotoptim.SpotOptim.SpotOptim.get_ocba_X }\n\n```python\nSpotOptim.SpotOptim.get_ocba_X(X, means, vars, delta, verbose=False)\n```\n\nCalculate OCBA allocation and repeat input array X.\n\n### get_pickle_safe_optimizer { #spotoptim.SpotOptim.SpotOptim.get_pickle_safe_optimizer }\n\n```python\nSpotOptim.SpotOptim.get_pickle_safe_optimizer(\n unpickleables='file_io',\n verbosity=0,\n)\n```\n\nCreate a pickle-safe copy of the optimizer.\n\n### get_ranks { #spotoptim.SpotOptim.SpotOptim.get_ranks }\n\n```python\nSpotOptim.SpotOptim.get_ranks(x)\n```\n\nReturns ranks of numbers within input array x.\n\n### get_result_filename { #spotoptim.SpotOptim.SpotOptim.get_result_filename }\n\n```python\nSpotOptim.SpotOptim.get_result_filename(prefix)\n```\n\nGenerate result filename with '_res.pkl' suffix.\n\n### get_results_table { #spotoptim.SpotOptim.SpotOptim.get_results_table }\n\n```python\nSpotOptim.SpotOptim.get_results_table(\n tablefmt='github',\n precision=4,\n show_importance=False,\n)\n```\n\nGet a comprehensive table string of optimization results.\nThis method generates a formatted table of the search space configuration,\nbest values found, and optionally variable importance scores.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------------|----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|\n| tablefmt | [str](`str`) | Table format for tabulate library. Options include: 'github', 'grid', 'simple', 'plain', 'html', 'latex', etc. Defaults to 'github'. | `'github'` |\n| precision | [int](`int`) | Number of decimal places for float values. Defaults to 4. | `4` |\n| show_importance | [bool](`bool`) | Whether to include importance scores. Importance is calculated as the normalized standard deviation of each parameter's effect on the objective. Requires multiple evaluations. Defaults to False. | `False` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------------|------------------------------------------------------|\n| str | [str](`str`) | Formatted table string that can be printed or saved. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#3ced231f .cell execution_count=34}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\n# Example 1: Basic usage after optimization\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5), (-5, 5)],\n var_name=[\"x1\", \"x2\", \"x3\"],\n var_type=[\"float\", \"float\", \"float\"],\n max_iter=10,\n n_initial=5\n)\nresult = opt.optimize()\ntable = opt.get_results_table()\nprint(table)\ntable = opt.get_results_table(show_importance=True)\nprint(table)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n| name | type | default | lower | upper | tuned | transform |\n|--------|--------|-----------|---------|---------|---------|-------------|\n| x1 | float | 0 | -5 | 5 | -0.2921 | - |\n| x2 | float | 0 | -5 | 5 | 0.2695 | - |\n| x3 | float | 0 | -5 | 5 | 0.0753 | - |\n| name | type | default | lower | upper | tuned | transform | importance | stars |\n|--------|--------|-----------|---------|---------|---------|-------------|--------------|---------|\n| x1 | float | 0 | -5 | 5 | -0.2921 | - | 56.84 | * |\n| x2 | float | 0 | -5 | 5 | 0.2695 | - | 18.44 | . |\n| x3 | float | 0 | -5 | 5 | 0.0753 | - | 24.72 | . |\n\nInterpretation: ***: >99%, **: >75%, *: >50%, .: >10%\n```\n:::\n:::\n\n\n### get_shape { #spotoptim.SpotOptim.SpotOptim.get_shape }\n\n```python\nSpotOptim.SpotOptim.get_shape(y)\n```\n\nGet the shape of the objective function output.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------------------------------------|------------|\n| y | [ndarray](`ndarray`) | Objective function output, shape (n_samples,) or (n_samples, n_objectives). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|----------------------------------------------------------------------------------------|----------------------------------------------------------------------------|\n| tuple | [Tuple](`typing.Tuple`)\\[[int](`int`), [Optional](`typing.Optional`)\\[[int](`int`)\\]\\] | (n_samples, n_objectives) where n_objectives is None for single-objective. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#0ab2ca1b .cell execution_count=35}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(\n fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5\n)\ny_single = np.array([1.0, 2.0, 3.0])\nn, m = opt.get_shape(y_single)\nprint(f\"n={n}, m={m}\")\ny_multi = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])\nn, m = opt.get_shape(y_multi)\nprint(f\"n={n}, m={m}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nn=3, m=None\nn=3, m=2\n```\n:::\n:::\n\n\n### get_stars { #spotoptim.SpotOptim.SpotOptim.get_stars }\n\n```python\nSpotOptim.SpotOptim.get_stars(input_list)\n```\n\nConverts a list of values to a list of stars.\nUsed to visualize the importance of a variable.\nThresholds: >99: ***, >75: **, >50: *, >10: .\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|------------|----------------|--------------------------------------|------------|\n| input_list | [list](`list`) | A list of importance scores (0-100). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|----------------|-------------------------|\n| list | [list](`list`) | A list of star strings. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#c2fbe63b .cell execution_count=36}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\n\ndef test_func(X):\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=10,\n n_initial=5,\n seed=42\n)\nopt.optimize()\nopt.get_stars([100, 75, 50, 10, 0])\n```\n\n::: {.cell-output .cell-output-display execution_count=36}\n```\n['***', '*', '.', '', '']\n```\n:::\n:::\n\n\n### get_success_rate { #spotoptim.SpotOptim.SpotOptim.get_success_rate }\n\n```python\nSpotOptim.SpotOptim.get_success_rate()\n```\n\nGet the current success rate of the optimization process.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|------------------|---------------------------|\n| float | [float](`float`) | The current success rate. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#e604d927 .cell execution_count=37}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda x: x,\n bounds=[(-5, 5), (-5, 5)])\nprint(opt.get_success_rate())\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n0.0\n```\n:::\n:::\n\n\n### handle_default_var_trans { #spotoptim.SpotOptim.SpotOptim.handle_default_var_trans }\n\n```python\nSpotOptim.SpotOptim.handle_default_var_trans()\n```\n\nHandle default variable transformations. Does not perform any transformations,\nonly sets `var_trans` to a list of `None` values if not specified, or normalizes\ntransformation names by converting `id`, `None`, or `None` to `None`.\nAlso validates that `var_trans` length matches the number of dimensions.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|------------------------------------------|\n| | [ValueError](`ValueError`) | If var_trans length doesn't match n_dim. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#e79a4630 .cell execution_count=38}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\n# Default behavior - all None\nspot = SpotOptim(fun=lambda x: x, bounds=[(0, 10), (0, 10)])\nprint(f\"spot.var_trans (should be [None, None]): {spot.var_trans}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nspot.var_trans (should be [None, None]): [None, None]\n```\n:::\n:::\n\n\n::: {#a0373001 .cell execution_count=39}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\n# Normalize transformation names\nspot = SpotOptim(fun=lambda x: x, bounds=[(1, 10), (1, 100)],\n var_trans=['log10', 'id'])\nprint(f\"spot.var_trans (should be ['log10', 'None']): {spot.var_trans}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nspot.var_trans (should be ['log10', 'None']): ['log10', None]\n```\n:::\n:::\n\n\n### init_storage { #spotoptim.SpotOptim.SpotOptim.init_storage }\n\n```python\nSpotOptim.SpotOptim.init_storage(X0, y0)\n```\n\nInitialize storage for optimization.\nSets up the initial data structures needed for optimization tracking:\n * X_: Evaluated design points (in original scale)\n * y_: Function values at evaluated points\n * n_iter_: Iteration counter\nThen updates statistics by calling `update_stats()`.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-------------------------------------------------------------------------|------------|\n| X0 | [ndarray](`ndarray`) | Initial design points in internal scale, shape (n_samples, n_features). | _required_ |\n| y0 | [ndarray](`ndarray`) | Function values at X0, shape (n_samples,). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#696db1ea .cell execution_count=40}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5)\nX0 = np.array([[1, 2], [3, 4], [0, 1]])\ny0 = np.array([5.0, 25.0, 1.0])\nopt.init_storage(X0, y0)\nprint(f\"X_ = {opt.X_}\")\nprint(f\"y_ = {opt.y_}\")\nprint(f\"n_iter_ = {opt.n_iter_}\")\nprint(f\"counter = {opt.counter}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nX_ = [[1. 2.]\n [3. 4.]\n [0. 1.]]\ny_ = [ 5. 25. 1.]\nn_iter_ = 0\ncounter = 0\n```\n:::\n:::\n\n\n### init_surrogate { #spotoptim.SpotOptim.SpotOptim.init_surrogate }\n\n```python\nSpotOptim.SpotOptim.init_surrogate()\n```\n\nInitialize or configure the surrogate model for optimization. Handles three surrogate configurations:\n * List of surrogates: sets up multi-surrogate selection with probability weights and per-surrogate `max_surrogate_points`.\n * None (default): creates a `GaussianProcessRegressor` with a\n `ConstantKernel * Matern(nu=2.5)` kernel, 100 optimizer restarts,\n and `normalize_y=True`.\n * User-provided surrogate: accepted as-is; internal bookkeeping\n attributes (`_max_surrogate_points_list`,\n `_active_max_surrogate_points`) are still initialised.\nAfter this method returns the following attributes are set:\n * `self.surrogate` — the active surrogate model.\n * `self._surrogates_list` — `list | None`.\n * `self._prob_surrogate` — normalised selection probabilities or `None`.\n * `self._max_surrogate_points_list` — per-surrogate point caps or `None`.\n * `self._active_max_surrogate_points` — active cap.\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|---------------------------------------------------------------------------------|\n| | [ValueError](`ValueError`) | If the surrogate list is empty. |\n| | [ValueError](`ValueError`) | If 'prob_surrogate' length does not match the surrogate list length. |\n| | [ValueError](`ValueError`) | If 'max_surrogate_points' list length does not match the surrogate list length. |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#4a4f55d1 .cell execution_count=41}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n# Default surrogate (GaussianProcessRegressor)\nopt = SpotOptim(\n fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n)\nprint(type(opt.surrogate).__name__)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nGaussianProcessRegressor\n```\n:::\n:::\n\n\n::: {#47a4a963 .cell execution_count=42}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom sklearn.ensemble import RandomForestRegressor\n# User-provided surrogate\nrf = RandomForestRegressor(n_estimators=50, random_state=42)\nopt = SpotOptim(\n fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n surrogate=rf,\n)\nprint(type(opt.surrogate).__name__)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nRandomForestRegressor\n```\n:::\n:::\n\n\n::: {#a75a4ad9 .cell execution_count=43}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom sklearn.ensemble import RandomForestRegressor\nfrom sklearn.gaussian_process import GaussianProcessRegressor\n# List of surrogates with selection probabilities\nsurrogates = [GaussianProcessRegressor(), RandomForestRegressor()]\nopt = SpotOptim(\n fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n surrogate=surrogates,\n prob_surrogate=[0.7, 0.3],\n)\nprint(opt._prob_surrogate)\nprint([type(s).__name__ for s in opt._surrogates_list])\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[0.7, 0.3]\n['GaussianProcessRegressor', 'RandomForestRegressor']\n```\n:::\n:::\n\n\n### inverse_transform_X { #spotoptim.SpotOptim.SpotOptim.inverse_transform_X }\n\n```python\nSpotOptim.SpotOptim.inverse_transform_X(X)\n```\n\nTransform parameter array from internal to original scale.\nConverts from transformed space (full dimension) to natural space (original).\nDoes NOT handle dimension expansion (un-mapping).\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Array in Transformed Space, shape (n_samples, n_features) | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Array in Natural Space |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#a4ec4965 .cell execution_count=44}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nimport numpy as np\nspot = SpotOptim(fun=sphere, bounds=[(1, 10)], var_trans=['log10'])\nX_trans = np.array([[0], [1], [2]])\nspot.inverse_transform_X(X_trans)\n```\n\n::: {.cell-output .cell-output-display execution_count=44}\n```\narray([[ 1],\n [ 10],\n [100]])\n```\n:::\n:::\n\n\n### inverse_transform_value { #spotoptim.SpotOptim.SpotOptim.inverse_transform_value }\n\n```python\nSpotOptim.SpotOptim.inverse_transform_value(x, trans)\n```\n\nApply inverse transformation to a single float value.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|-----------------------------------------------|----------------------|------------|\n| x | [float](`float`) | Transformed value | _required_ |\n| trans | [Optional](`typing.Optional`)\\[[str](`str`)\\] | Transformation name. | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|------------------|----------------|\n| | [float](`float`) | Original value |\n\n#### Notes {.doc-section .doc-section-notes}\n\nSee also transform_value.\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#b0377034 .cell execution_count=45}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nspot = SpotOptim(fun=sphere, bounds=[(1, 10)])\nspot.inverse_transform_value(10, 'log10')\nspot.inverse_transform_value(100, 'log(x)')\n```\n\n::: {.cell-output .cell-output-display execution_count=45}\n```\nnp.float64(2.6881171418161356e+43)\n```\n:::\n:::\n\n\n### load_experiment { #spotoptim.SpotOptim.SpotOptim.load_experiment }\n\n```python\nSpotOptim.SpotOptim.load_experiment(filename)\n```\n\nLoad experiment configuration from a pickle file.\n\n### load_result { #spotoptim.SpotOptim.SpotOptim.load_result }\n\n```python\nSpotOptim.SpotOptim.load_result(filename)\n```\n\nLoad complete optimization results from a pickle file.\n\n### map_to_factor_values { #spotoptim.SpotOptim.SpotOptim.map_to_factor_values }\n\n```python\nSpotOptim.SpotOptim.map_to_factor_values(X)\n```\n\nMap internal integer factor values back to string labels.\nFor factor variables, converts integer indices back to original string values.\nOther variable types remain unchanged.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-------------------------------------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Design points with integer values for factors, shape (n_samples, n_features). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Design points with factor integers replaced by string labels. Dtype will be object or string if mixed types are present. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#a337f7c4 .cell execution_count=46}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nimport numpy as np\nspot = SpotOptim(\n fun=sphere,\n bounds=[('red', 'blue'), (0, 10)]\n)\nspot.process_factor_bounds()\nX_int = np.array([[0, 5.0], [1, 8.0]])\nX_str = spot.map_to_factor_values(X_int)\nprint(X_str[0])\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n['red' 5.0]\n```\n:::\n:::\n\n\n### mo2so { #spotoptim.SpotOptim.SpotOptim.mo2so }\n\n```python\nSpotOptim.SpotOptim.mo2so(y_mo)\n```\n\nConvert multi-objective values to single-objective.\nConverts multi-objective values to a single-objective value by applying a user-defined\nfunction from `fun_mo2so`. If no user-defined function is given, the\nvalues in the first objective column are used.\n\nThis method is called after the objective function evaluation. It returns a 1D array\nwith the single-objective values.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|----------------------------------------------------------------------------------------------------------|------------|\n| y_mo | [ndarray](`ndarray`) | If multi-objective, shape (n_samples, n_objectives). If single-objective, shape (n_samples,). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|----------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Single-objective values, shape (n_samples,). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#769f687b .cell execution_count=47}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\n# Multi-objective function\ndef mo_fun(X):\n return np.column_stack([\n np.sum(X**2, axis=1),\n np.sum((X-1)**2, axis=1)\n ])\n\n# Example 1: Default behavior (use first objective)\nopt1 = SpotOptim(\n fun=mo_fun,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5\n)\ny_mo = np.array([[1.0, 2.0], [3.0, 4.0]])\ny_so = opt1.mo2so(y_mo)\nprint(f\"Single-objective (default): {y_so}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSingle-objective (default): [1. 3.]\n```\n:::\n:::\n\n\n::: {#763ab0aa .cell execution_count=48}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n# Example 2: Custom conversion function (sum of objectives)\ndef custom_mo2so(y_mo):\n return y_mo[:, 0] + y_mo[:, 1]\n\nopt2 = SpotOptim(\n fun=mo_fun,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5,\n fun_mo2so=custom_mo2so\n)\ny_so_custom = opt2.mo2so(y_mo)\nprint(f\"Single-objective (custom): {y_so_custom}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSingle-objective (custom): [3. 7.]\n```\n:::\n:::\n\n\n### modify_bounds_based_on_var_type { #spotoptim.SpotOptim.SpotOptim.modify_bounds_based_on_var_type }\n\n```python\nSpotOptim.SpotOptim.modify_bounds_based_on_var_type()\n```\n\nModify bounds based on variable types.\nAdjusts bounds for each dimension according to its var_type:\n * 'int': Ensures bounds are integers (ceiling for lower, floor for upper)\n * 'factor': Bounds already set to (0, n_levels-1) by process_factor_bounds\n * 'float': Explicitly converts bounds to float\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|--------------------------------------------|\n| | [ValueError](`ValueError`) | If an unsupported var_type is encountered. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#e767b87a .cell execution_count=49}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nspot = SpotOptim(fun=lambda x: x, bounds=[(0.5, 10.5)], var_type=['int'])\nprint(spot.bounds)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[(1, 10)]\n```\n:::\n:::\n\n\n::: {#42cb35ba .cell execution_count=50}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nspot = SpotOptim(fun=lambda x: x, bounds=[(0, 10)], var_type=['float'])\nprint(spot.bounds)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[(0.0, 10.0)]\n```\n:::\n:::\n\n\n### optimize { #spotoptim.SpotOptim.SpotOptim.optimize }\n\n```python\nSpotOptim.SpotOptim.optimize(X0=None)\n```\n\nRun the optimization process. The optimization terminates when either the total function evaluations reach\n `max_iter` (including initial design), or the runtime exceeds max_time minutes. Input/Output spaces are\n * Input `X0`: Expected in Natural Space (original scale, physical units).\n * Output `result.x`: Returned in Natural Space.\n * Output `result.X`: Returned in Natural Space.\n * Internal Optimization: Performed in Transformed and Mapped Space.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------------------------------------------------------------------------------------------|-----------|\n| X0 | [ndarray](`ndarray`) | Initial design points in Natural Space, shape (n_initial, n_features). If None, generates space-filling design. Defaults to None. | `None` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|----------------|---------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| OptimizeResult | [OptimizeResult](`scipy.optimize.OptimizeResult`) | Optimization result with fields: * x: best point found in Natural Space * fun: best function value * nfev: number of function evaluations (including initial design) * nit: number of sequential optimization iterations (after initial design) * success: whether optimization succeeded * message: termination message indicating reason for stopping, including statistics (function value, iterations, evaluations) * X: all evaluated points in Natural Space * y: all function values |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#36dc8836 .cell execution_count=51}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n max_iter=10,\n seed=0,\n x0=np.array([0.1, -0.1]),\n verbose=True\n)\nresult = opt.optimize()\nprint(result.message.splitlines()[0])\nprint(\"Best point:\", result.x)\nprint(\"Best value:\", result.fun)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nStarting point x0 validated and processed successfully.\n Original scale: [ 0.1 -0.1]\n Internal scale: [ 0.1 -0.1]\nTensorBoard logging disabled\nIncluding 1 starting points from x0 in initial design.\nInitial best: f(x) = 0.020000\nIter 1 | Best: 0.020000 | Curr: 14.707944 | Rate: 0.00 | Evals: 60.0%\nOptimizer candidate 1/3 was duplicate/invalid.\nIter 2 | Best: 0.020000 | Curr: 0.020020 | Rate: 0.00 | Evals: 70.0%\nIter 3 | Best: 0.020000 | Curr: 0.322921 | Rate: 0.00 | Evals: 80.0%\nIter 4 | Best: 0.002244 | Rate: 0.25 | Evals: 90.0%\nIter 5 | Best: 0.002179 | Rate: 0.40 | Evals: 100.0%\nOptimization terminated: maximum evaluations (10) reached\nBest point: [0.04474339 0.01330855]\nBest value: 0.002179088112986503\n```\n:::\n:::\n\n\n### optimize_acquisition_func { #spotoptim.SpotOptim.SpotOptim.optimize_acquisition_func }\n\n```python\nSpotOptim.SpotOptim.optimize_acquisition_func()\n```\n\nOptimize the acquisition function to find the next point to evaluate.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | The optimized point(s). If acquisition_fun_return_size == 1, returns 1D array of shape (n_features,). If acquisition_fun_return_size > 1, returns 2D array of shape (N, n_features), where N is min(acquisition_fun_return_size, population_size). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#283531fa .cell execution_count=52}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n max_iter=10,\n seed=0,\n)\nopt.optimize()\nx_next = opt.suggest_next_infill_point()\nprint(\"Next point to evaluate:\", x_next)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nNext point to evaluate: [[0.14356455 0.03793884]]\n```\n:::\n:::\n\n\n### optimize_sequential_run { #spotoptim.SpotOptim.SpotOptim.optimize_sequential_run }\n\n```python\nSpotOptim.SpotOptim.optimize_sequential_run(\n timeout_start,\n X0=None,\n y0_known=None,\n max_iter_override=None,\n)\n```\n\nPerform a single sequential optimization run.\nCalls _initialize_run, rm_initial_design_NA_values, check_size_initial_design, init_storage, get_best_xy_initial_design, and _run_sequential_loop.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-------------------|---------------------------------------------------------------------------|------------------------------------------------------------------------|------------|\n| timeout_start | [float](`float`) | Start time for timeout. | _required_ |\n| X0 | [Optional](`typing.Optional`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`)\\] | Initial design points in Natural Space, shape (n_initial, n_features). | `None` |\n| y0_known | [Optional](`typing.Optional`)\\[[float](`float`)\\] | Known best value for initial design. | `None` |\n| max_iter_override | [Optional](`typing.Optional`)\\[[int](`int`)\\] | Override for maximum number of iterations. | `None` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|\n| | [Tuple](`typing.Tuple`)\\[[str](`str`), [OptimizeResult](`scipy.optimize.OptimizeResult`)\\] | Tuple[str, OptimizeResult]: Tuple containing status and optimization result. |\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|----------------------------------------------------------------------------------------------------------------------------|\n| | [ValueError](`ValueError`) | If the initial design has no valid points after removing NaN/inf values, or if the initial design is too small to proceed. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#6cefd235 .cell execution_count=53}\n``` {.python .cell-code}\nimport time\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n max_iter=10,\n seed=0,\n verbose=True\n )\nstatus, result = opt.optimize_sequential_run(timeout_start=time.time())\nprint(status)\nprint(result.message.splitlines()[0])\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 8.463203\nIter 1 | Best: 8.463203 | Curr: 18.224245 | Rate: 0.00 | Evals: 60.0%\nIter 2 | Best: 8.412459 | Rate: 0.50 | Evals: 70.0%\nIter 3 | Best: 5.623369 | Rate: 0.67 | Evals: 80.0%\nIter 4 | Best: 2.902974 | Rate: 0.75 | Evals: 90.0%\nIter 5 | Best: 0.022050 | Rate: 0.80 | Evals: 100.0%\nFINISHED\nOptimization terminated: maximum evaluations (10) reached\n```\n:::\n:::\n\n\n### plot_importance { #spotoptim.SpotOptim.SpotOptim.plot_importance }\n\n```python\nSpotOptim.SpotOptim.plot_importance(threshold=0.0, figsize=(10, 6))\n```\n\nPlot variable importance.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------|------------------|---------------------------------------------------|-----------|\n| threshold | [float](`float`) | Minimum importance percentage to include in plot. | `0.0` |\n| figsize | [tuple](`tuple`) | Figure size. | `(10, 6)` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#cb7b7f3a .cell execution_count=54}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\n\ndef test_func(X):\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=10,\n n_initial=5,\n seed=42\n)\nopt.optimize()\nopt.plot_importance()\n```\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim.SpotOptim_files/figure-html/cell-55-output-1.png){width=789 height=523}\n:::\n:::\n\n\n### plot_important_hyperparameter_contour { #spotoptim.SpotOptim.SpotOptim.plot_important_hyperparameter_contour }\n\n```python\nSpotOptim.SpotOptim.plot_important_hyperparameter_contour(\n max_imp=3,\n show=True,\n alpha=0.8,\n cmap='jet',\n num=100,\n add_points=True,\n grid_visible=True,\n contour_levels=30,\n figsize=(12, 10),\n)\n```\n\nPlot surrogate contours using spotoptim.plot.visualization.plot_important_hyperparameter_contour.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|----------------|------------------|----------------------------------------------------------|------------|\n| max_imp | [int](`int`) | The maximum number of important hyperparameters to plot. | `3` |\n| show | [bool](`bool`) | Whether to show the plot. | `True` |\n| alpha | [float](`float`) | The alpha value for the plot. | `0.8` |\n| cmap | [str](`str`) | The colormap to use. | `'jet'` |\n| num | [int](`int`) | The number of points to use for the plot. | `100` |\n| add_points | [bool](`bool`) | Whether to add points to the plot. | `True` |\n| grid_visible | [bool](`bool`) | Whether to show the grid. | `True` |\n| contour_levels | [int](`int`) | The number of contour levels to use. | `30` |\n| figsize | [tuple](`tuple`) | The size of the plot. | `(12, 10)` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#dd219443 .cell execution_count=55}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\n\ndef test_func(X):\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\n# 2-D problem: max_imp must not exceed n_dim (2)\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=10,\n n_initial=5,\n seed=42\n)\nopt.optimize()\nopt.plot_important_hyperparameter_contour(max_imp=2)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nPlotting surrogate contours for top 2 most important parameters:\n x0: importance = 73.24% (type: float)\n x1: importance = 26.76% (type: float)\n\nGenerating 1 surrogate plots...\n Plotting x0 vs x1\n```\n:::\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim.SpotOptim_files/figure-html/cell-56-output-2.png){width=1113 height=950}\n:::\n:::\n\n\n### plot_parameter_scatter { #spotoptim.SpotOptim.SpotOptim.plot_parameter_scatter }\n\n```python\nSpotOptim.SpotOptim.plot_parameter_scatter(\n result=None,\n show=True,\n figsize=(12, 10),\n ylabel='Objective Value',\n cmap='viridis_r',\n show_correlation=False,\n log_y=False,\n)\n```\n\nPlot parameter distributions showing relationship between each parameter and objective.\nCreates a grid of scatter plots, one for each parameter dimension, showing how\nthe objective function value varies with each parameter. The best configuration\nis marked with a red star. Parameters with log-scale transformations (var_trans)\nare automatically displayed on a log x-axis.\nOptionally displays Spearman correlation coefficients in plot titles for\nsensitivity analysis. For factor (categorical) variables, correlation is not\ncomputed and they are displayed with discrete positions on the x-axis.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|------------------|---------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|---------------------|\n| result | [OptimizeResult](`scipy.optimize.OptimizeResult`) | Optimization result containing best parameters. If None, uses the best found values from self.best_x_ and self.best_y_. | `None` |\n| show | [bool](`bool`) | Whether to display the plot. Defaults to True. | `True` |\n| figsize | [tuple](`tuple`) | Figure size as (width, height). Defaults to (12, 10). | `(12, 10)` |\n| ylabel | [str](`str`) | Label for y-axis. Defaults to \"Objective Value\". | `'Objective Value'` |\n| cmap | [str](`str`) | Colormap for scatter plot. Defaults to \"viridis_r\". | `'viridis_r'` |\n| show_correlation | [bool](`bool`) | Whether to compute and display Spearman correlation coefficients in plot titles. Requires scipy. Defaults to False. | `False` |\n| log_y | [bool](`bool`) | Whether to use logarithmic scale for y-axis. Defaults to False. | `False` |\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|---------------------------------------|\n| | [ValueError](`ValueError`) | If no optimization data is available. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#f74f2edd .cell execution_count=56}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef objective(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(\n fun=objective,\n bounds=[(-5, 5), (-5, 5), (-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\", \"x2\", \"x3\"],\n max_iter=10,\n n_initial=5,\n seed=42\n)\nresult = opt.optimize()\n# Plot parameter distributions\nopt.plot_parameter_scatter(result)\n# Plot with custom settings\nopt.plot_parameter_scatter(result, cmap=\"plasma\", ylabel=\"Error\")\n```\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim.SpotOptim_files/figure-html/cell-57-output-1.png){width=1141 height=949}\n:::\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim.SpotOptim_files/figure-html/cell-57-output-2.png){width=1141 height=949}\n:::\n:::\n\n\n### plot_progress { #spotoptim.SpotOptim.SpotOptim.plot_progress }\n\n```python\nSpotOptim.SpotOptim.plot_progress(\n show=True,\n log_y=False,\n figsize=(10, 6),\n ylabel='Objective Value',\n mo=False,\n)\n```\n\nPlot optimization progress using spotoptim.plot.visualization.plot_progress.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|---------|------------------|----------------------------------------------|---------------------|\n| show | [bool](`bool`) | Whether to show the plot. | `True` |\n| log_y | [bool](`bool`) | Whether to use a logarithmic y-axis. | `False` |\n| figsize | [tuple](`tuple`) | The size of the plot. | `(10, 6)` |\n| ylabel | [str](`str`) | The label for the y-axis. | `'Objective Value'` |\n| mo | [bool](`bool`) | Whether the optimization is multi-objective. | `False` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#481b4482 .cell execution_count=57}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\n\ndef test_func(X):\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=10,\n n_initial=5,\n seed=42\n)\nopt.optimize()\nopt.plot_progress()\n```\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim.SpotOptim_files/figure-html/cell-58-output-1.png){width=949 height=565}\n:::\n:::\n\n\n### plot_surrogate { #spotoptim.SpotOptim.SpotOptim.plot_surrogate }\n\n```python\nSpotOptim.SpotOptim.plot_surrogate(\n i=0,\n j=1,\n show=True,\n alpha=0.8,\n var_name=None,\n cmap='jet',\n num=100,\n vmin=None,\n vmax=None,\n add_points=True,\n grid_visible=True,\n contour_levels=30,\n figsize=(12, 10),\n)\n```\n\nPlot the surrogate model for two dimensions.\nDelegates to spotoptim.plot.visualization.plot_surrogate.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|----------------|------------------------------------------------------------------------|-------------------------------------------|------------|\n| i | [int](`int`) | The index of the first dimension. | `0` |\n| j | [int](`int`) | The index of the second dimension. | `1` |\n| show | [bool](`bool`) | Whether to show the plot. | `True` |\n| alpha | [float](`float`) | The alpha value for the plot. | `0.8` |\n| var_name | [Optional](`typing.Optional`)\\[[List](`typing.List`)\\[[str](`str`)\\]\\] | The names of the variables. | `None` |\n| cmap | [str](`str`) | The colormap to use. | `'jet'` |\n| num | [int](`int`) | The number of points to use for the plot. | `100` |\n| vmin | [Optional](`typing.Optional`)\\[[float](`float`)\\] | The minimum value for the plot. | `None` |\n| vmax | [Optional](`typing.Optional`)\\[[float](`float`)\\] | The maximum value for the plot. | `None` |\n| add_points | [bool](`bool`) | Whether to add points to the plot. | `True` |\n| grid_visible | [bool](`bool`) | Whether to show the grid. | `True` |\n| contour_levels | [int](`int`) | The number of contour levels to use. | `30` |\n| figsize | [tuple](`tuple`) | The size of the plot. | `(12, 10)` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#65a5409a .cell execution_count=58}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\n\ndef test_func(X):\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=10,\n n_initial=5,\n seed=42\n)\nopt.optimize()\nopt.plot_surrogate()\n```\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim.SpotOptim_files/figure-html/cell-59-output-1.png){width=1113 height=950}\n:::\n:::\n\n\n### print_best { #spotoptim.SpotOptim.SpotOptim.print_best }\n\n```python\nSpotOptim.SpotOptim.print_best(\n result=None,\n transformations=None,\n show_name=True,\n precision=4,\n)\n```\n\nPrint the best solution found during optimization.\nThis method displays the best hyperparameters and objective value in a\nformatted table. It supports custom transformations for parameters\n(e.g., converting log-scale values back to original scale).\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------------|---------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------|\n| result | [OptimizeResult](`scipy.optimize.OptimizeResult`) | Optimization result object from optimize(). If None, uses the stored best values from the optimizer. Defaults to None. | `None` |\n| transformations | list of callable | List of transformation functions to apply to each parameter. Each function takes a single value and returns the transformed value. Use None for parameters that don't need transformation. Length must match number of dimensions. Example: [None, None, lambda x: 10**x] to convert the 3rd parameter from log10 scale. Defaults to None. | `None` |\n| show_name | [bool](`bool`) | Whether to display variable names. If False, uses generic names like 'x0', 'x1', etc. Defaults to True. | `True` |\n| precision | [int](`int`) | Number of decimal places for floating point values. Defaults to 4. | `4` |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#d153d144 .cell execution_count=59}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x1\", \"x2\"],\n max_iter=10,\n n_initial=5\n)\nresult = opt.optimize()\nopt.print_best(result)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n\nBest Solution Found:\n--------------------------------------------------\n x1: -1.1704\n x2: -0.4434\n Objective Value: 1.5664\n Total Evaluations: 10\n```\n:::\n:::\n\n\n### print_results { #spotoptim.SpotOptim.SpotOptim.print_results }\n\n```python\nSpotOptim.SpotOptim.print_results(*args, **kwargs)\n```\n\nAlias for print(get_results_table()) for compatibility.\nPrints the table.\n\n### process_factor_bounds { #spotoptim.SpotOptim.SpotOptim.process_factor_bounds }\n\n```python\nSpotOptim.SpotOptim.process_factor_bounds()\n```\n\nProcess `bounds` to handle factor variables.\nFor dimensions with tuple bounds (factor variables), creates internal\ninteger mappings and replaces bounds with (0, n_levels-1).\nStores mappings in `self._factor_maps`: {dim_idx: {int_val: str_val}}\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|------------------------------------|\n| | [ValueError](`ValueError`) | If bounds are invalidly formatted. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#0792bc57 .cell execution_count=60}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nspot = SpotOptim(fun=lambda x: x, bounds=[('red', 'green', 'blue'), (0, 10)])\nspot.process_factor_bounds()\nprint(f\"spot.bounds (should be [(0, 2), (0, 10)]): {spot.bounds}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nspot.bounds (should be [(0, 2), (0, 10)]): [(0, 2), (0, 10)]\n```\n:::\n:::\n\n\n### reinitialize_components { #spotoptim.SpotOptim.SpotOptim.reinitialize_components }\n\n```python\nSpotOptim.SpotOptim.reinitialize_components()\n```\n\nReinitialize components that were excluded during pickling.\n\n### remove_nan { #spotoptim.SpotOptim.SpotOptim.remove_nan }\n\n```python\nSpotOptim.SpotOptim.remove_nan(X, y, stop_on_zero_return=True)\n```\n\nRemove rows where y contains NaN or inf values.\nUsed in the optimize() method after function evaluations.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|---------------------|----------------------|---------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Design matrix, shape (n_samples, n_features). | _required_ |\n| y | [ndarray](`ndarray`) | Objective values, shape (n_samples,). | _required_ |\n| stop_on_zero_return | [bool](`bool`) | If True, raise error when all values are removed. | `True` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|------------------|-----------------------------------------------|\n| tuple | [tuple](`tuple`) | (X_clean, y_clean) with NaN/inf rows removed. |\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|------------------------------------------------------------|\n| | [ValueError](`ValueError`) | If all values are NaN/inf and stop_on_zero_return is True. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#b5e217b7 .cell execution_count=61}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nX = np.array([[1, 2], [3, 4], [5, 6]])\ny = np.array([1.0, np.nan, np.inf])\nX_clean, y_clean = opt.remove_nan(X, y, stop_on_zero_return=False)\nprint(\"Clean X:\", X_clean)\nprint(\"Clean y:\", y_clean)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nClean X: [[1 2]]\nClean y: [1.]\n```\n:::\n:::\n\n\n### repair_natural_X { #spotoptim.SpotOptim.SpotOptim.repair_natural_X }\n\n```python\nSpotOptim.SpotOptim.repair_natural_X(X)\n```\n\nEnforce integrality and declared bounds in natural (original) space.\n\nInteger dimensions are rounded to the nearest integer; integer\ndimensions with an active transform are additionally clipped to their\ndeclared natural bounds, because the inverse transform of a continuous\ninternal proposal can land marginally outside them (issue #87). Float\nand factor dimensions pass through unchanged.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|--------------------------------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Points in natural scale, shape (n_samples, n_features) or (n_features,). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|---------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Repaired copy of ``X`` (same shape as the input). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#c30bfcf9 .cell execution_count=62}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(10, 5000)],\n var_type=['int'],\n var_trans=['log10'])\nX_nat = np.array([[4999.99999], [10.4], [5000.2]])\nprint(opt.repair_natural_X(X_nat))\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[[5000.]\n [ 10.]\n [5000.]]\n```\n:::\n:::\n\n\n### repair_non_numeric { #spotoptim.SpotOptim.SpotOptim.repair_non_numeric }\n\n```python\nSpotOptim.SpotOptim.repair_non_numeric(X, var_type)\n```\n\nRound non-numeric values to integers based on variable type.\nThis method applies rounding to variables that are not continuous:\n * 'float': No rounding (continuous values)\n * 'int': Rounded to integers\n * 'factor': Rounded to integers (representing categorical values)\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|----------|----------------------|------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | X array with values to potentially round. | _required_ |\n| var_type | list of str | List with type information for each dimension. | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|---------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | X array with non-continuous values rounded to integers. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#97636316 .cell execution_count=63}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n var_type=['int', 'float'])\nX = np.array([[1.2, 2.5], [3.7, 4.1], [5.9, 6.8]])\nX_repaired = opt.repair_non_numeric(X, opt.var_type)\nprint(X_repaired)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[[1. 2.5]\n [4. 4.1]\n [6. 6.8]]\n```\n:::\n:::\n\n\n### rm_initial_design_NA_values { #spotoptim.SpotOptim.SpotOptim.rm_initial_design_NA_values }\n\n```python\nSpotOptim.SpotOptim.rm_initial_design_NA_values(X0, y0)\n```\n\nRemove NaN/inf values from initial design evaluations.\nThis method filters out design points that returned NaN or inf values\nduring initial evaluation. Unlike the sequential optimization phase where\npenalties are applied, initial design points with invalid values are\nsimply removed.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-------------------------------------------------------------------------|------------|\n| X0 | [ndarray](`ndarray`) | Initial design points in internal scale, shape (n_samples, n_features). | _required_ |\n| y0 | [ndarray](`ndarray`) | Function values at X0, shape (n_samples,). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| | [Tuple](`typing.Tuple`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`), [int](`int`)\\] | Tuple[ndarray, ndarray, int]: Filtered (X0, y0) with only finite values and the original count before filtering. X0 has shape (n_valid, n_features), y0 has shape (n_valid,), and the int is the original size. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#d95a9132 .cell execution_count=64}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=10\n)\nX0 = np.array([[1, 2], [3, 4], [5, 6]])\ny0 = np.array([5.0, np.nan, np.inf])\nX0_clean, y0_clean, n_eval = opt.rm_initial_design_NA_values(X0, y0)\nprint(X0_clean.shape) # (1, 2)\nprint(y0_clean) # array([5.])\nprint(n_eval) # 3\n# All valid values - no filtering\nX0 = np.array([[1, 2], [3, 4]])\ny0 = np.array([5.0, 25.0])\nX0_clean, y0_clean, n_eval = opt.rm_initial_design_NA_values(X0, y0)\nprint(X0_clean.shape) # (2, 2)\nprint(n_eval) # 2\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(1, 2)\n[5.]\n3\n(2, 2)\n2\n```\n:::\n:::\n\n\n### save_experiment { #spotoptim.SpotOptim.SpotOptim.save_experiment }\n\n```python\nSpotOptim.SpotOptim.save_experiment(\n filename=None,\n prefix='experiment',\n path=None,\n overwrite=True,\n unpickleables='all',\n verbosity=0,\n)\n```\n\nSave experiment configuration to a pickle file.\n\n### save_result { #spotoptim.SpotOptim.SpotOptim.save_result }\n\n```python\nSpotOptim.SpotOptim.save_result(\n filename=None,\n prefix='result',\n path=None,\n overwrite=True,\n verbosity=0,\n)\n```\n\nSave complete optimization results to a pickle file.\n\n### select_new { #spotoptim.SpotOptim.SpotOptim.select_new }\n\n```python\nSpotOptim.SpotOptim.select_new(A, X, tolerance=0)\n```\n\nSelect rows from A that are not in X.\nUsed in suggest_next_infill_point() to avoid duplicate evaluations.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------|----------------------|------------------------------------------------|------------|\n| A | [ndarray](`ndarray`) | Array with new values. | _required_ |\n| X | [ndarray](`ndarray`) | Array with known values. | _required_ |\n| tolerance | [float](`float`) | Tolerance value for comparison. Defaults to 0. | `0` |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|---------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|\n| tuple | [Tuple](`typing.Tuple`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`)\\] | A tuple containing: * ndarray: Array with unknown (new) values. * ndarray: Array with True if value is new, otherwise False. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#09472aea .cell execution_count=65}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nA = np.array([[1, 2], [3, 4], [5, 6]])\nX = np.array([[3, 4], [7, 8]])\nnew_A, is_new = opt.select_new(A, X)\nprint(\"New A:\", new_A)\nprint(\"Is new:\", is_new)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nNew A: [[1 2]\n [5 6]]\nIs new: [ True False True]\n```\n:::\n:::\n\n\n### sensitivity_spearman { #spotoptim.SpotOptim.SpotOptim.sensitivity_spearman }\n\n```python\nSpotOptim.SpotOptim.sensitivity_spearman()\n```\n\nCompute and print Spearman correlation between parameters and objective values.\nThis method analyzes the sensitivity of the objective function to each\nhyperparameter by computing Spearman rank correlations. For categorical\n(factor) variables, correlation is not computed as they require visual\ninspection instead.\nThe method automatically handles different parameter types:\n * Integer/float parameters: Direct correlation with objective values\n * Log-transformed parameters (log10, log, ln): Correlation in log-space\n * Factor (categorical) parameters: Skipped with informative message\nSignificance levels:\n * ***: p < 0.001 (highly significant)\n * **: p < 0.01 (significant)\n * *: p < 0.05 (marginally significant)\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#45356c33 .cell execution_count=66}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\n\ndef test_func(X):\n # x0 has strong effect, x1 has weak effect\n X = np.atleast_2d(X)\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=10,\n n_initial=5,\n seed=42\n)\nopt.optimize()\nopt.sensitivity_spearman()\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n\nSensitivity Analysis (Spearman Correlation):\n--------------------------------------------------\n x0 : -0.188 (p=0.603)\n x1 : -0.297 (p=0.405)\n```\n:::\n:::\n\n\n#### Note {.doc-section .doc-section-note}\n\nOnly meaningful after optimize() has been called with sufficient evaluations.\n\n### set_seed { #spotoptim.SpotOptim.SpotOptim.set_seed }\n\n```python\nSpotOptim.SpotOptim.set_seed()\n```\n\nSet global random seeds for reproducibility.\nSets seeds for:\n * random\n * numpy.random\n * torch (cpu and cuda)\nOnly performs actions if self.seed is not None.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#b6262867 .cell execution_count=67}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\nspot = SpotOptim(fun=lambda x: x, bounds=[(0, 1)], seed=42)\nspot.set_seed()\nnp.random.rand() # Should be deterministic\n```\n\n::: {.cell-output .cell-output-display execution_count=67}\n```\n0.3745401188473625\n```\n:::\n:::\n\n\n### setup_dimension_reduction { #spotoptim.SpotOptim.SpotOptim.setup_dimension_reduction }\n\n```python\nSpotOptim.SpotOptim.setup_dimension_reduction()\n```\n\nSet up dimension reduction by identifying fixed dimensions.\nIdentifies dimensions where lower and upper bounds are equal in Transformed Space.\nReduces `self.bounds`, `self.lower`, `self.upper`, etc., to the Mapped Space\n(active variables only).\nThe resulting `self.bounds` defines the Transformed and Mapped Space used\nfor optimization.\nThis method identifies variables that are fixed (constant) and excludes them\nfrom the optimization process. It stores:\n * Original bounds and metadata in `all_*` attributes\n * Boolean mask of fixed dimensions in `ident`\n * Reduced bounds, types, and names for optimization\n * `red_dim` flag indicating if reduction occurred\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#158630f9 .cell execution_count=68}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nspot = SpotOptim(fun=lambda x: x, bounds=[(1, 10), (5, 5), (0, 1)])\nprint(\"Original lower bounds:\", spot.all_lower)\nprint(\"Original upper bounds:\", spot.all_upper)\nprint(\"Fixed dimensions mask:\", spot.ident)\nprint(\"Reduced lower bounds:\", spot.lower)\nprint(\"Reduced upper bounds:\", spot.upper)\nprint(\"Reduced variable names:\", spot.var_name)\nprint(\"Is dimension reduction active?\", spot.red_dim)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nOriginal lower bounds: [1. 5. 0.]\nOriginal upper bounds: [10. 5. 1.]\nFixed dimensions mask: [False True False]\nReduced lower bounds: [1. 0.]\nReduced upper bounds: [10. 1.]\nReduced variable names: ['x0', 'x2']\nIs dimension reduction active? True\n```\n:::\n:::\n\n\n### store_mo { #spotoptim.SpotOptim.SpotOptim.store_mo }\n\n```python\nSpotOptim.SpotOptim.store_mo(y_mo)\n```\n\nStore multi-objective values in self.y_mo.\nIf multi-objective values are present (ndim==2), they are stored in self.y_mo.\nNew values are appended to existing ones. For single-objective problems,\nself.y_mo remains None.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|----------------------------------------------------------------------------------------------------------|------------|\n| y_mo | [ndarray](`ndarray`) | If multi-objective, shape (n_samples, n_objectives). If single-objective, shape (n_samples,). | _required_ |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#e8dd2be1 .cell execution_count=69}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(\n fun=lambda X: np.column_stack([\n np.sum(X**2, axis=1),\n np.sum((X-1)**2, axis=1)\n ]),\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5\n)\ny_mo_1 = np.array([[1.0, 2.0], [3.0, 4.0]])\nopt.store_mo(y_mo_1)\nprint(f\"y_mo after first call: {opt.y_mo}\")\ny_mo_2 = np.array([[5.0, 6.0], [7.0, 8.0]])\nopt.store_mo(y_mo_2)\nprint(f\"y_mo after second call: {opt.y_mo}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ny_mo after first call: [[1. 2.]\n [3. 4.]]\ny_mo after second call: [[1. 2.]\n [3. 4.]\n [5. 6.]\n [7. 8.]]\n```\n:::\n:::\n\n\n### suggest_next_infill_point { #spotoptim.SpotOptim.SpotOptim.suggest_next_infill_point }\n\n```python\nSpotOptim.SpotOptim.suggest_next_infill_point()\n```\n\nSuggest next point to evaluate (dispatcher).\nUsed in both sequential and parallel optimization loops. This method orchestrates\nthe process of generating candidate points from the acquisition function optimizer,\nhandling any failures in the acquisition process with a fallback strategy, and\nensuring that the returned point(s) are valid and ready for evaluation.\nThe returned point is in the Transformed and Mapped Space (Internal Optimization Space).\nThis means:\n 1. Transformations (e.g., log, sqrt) have been applied.\n 2. Dimension reduction has been applied (fixed variables removed).\nProcess:\n 1. Try candidates from acquisition function optimizer.\n 2. Handle acquisition failure (fallback).\n 3. Return last attempt if all fails.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Next point(s) to evaluate in Transformed and Mapped Space. |\n| | [np](`numpy`).[ndarray](`numpy.ndarray`) | Shape is (n_infill_points, n_features). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#dcd65ad6 .cell execution_count=70}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n n_infill_points=2\n)\n# Need to initialize optimization state (X_, y_, surrogate)\n# Normally done inside optimize()\nnp.random.seed(0)\nopt.X_ = np.random.rand(10, 2)\nopt.y_ = np.random.rand(10)\nopt.fit_surrogate(opt.X_, opt.y_)\nx_next = opt.suggest_next_infill_point()\nx_next.shape\n```\n\n::: {.cell-output .cell-output-display execution_count=70}\n```\n(2, 2)\n```\n:::\n:::\n\n\n### to_all_dim { #spotoptim.SpotOptim.SpotOptim.to_all_dim }\n\n```python\nSpotOptim.SpotOptim.to_all_dim(X_red)\n```\n\nExpand reduced-dimensional points to full-dimensional representation.\nThis method restores points from the reduced optimization space to the\nfull-dimensional space by inserting fixed values for constant dimensions.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-------------------------------------------------------------|------------|\n| X_red | [ndarray](`ndarray`) | Points in reduced space, shape (n_samples, n_reduced_dims). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|-----------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Points in full space, shape (n_samples, n_original_dims). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#353b240c .cell execution_count=71}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n# Create problem with one fixed dimension\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (2, 2), (-5, 5)], # x1 is fixed at 2\n max_iter=10,\n n_initial=3\n)\nX_red = np.array([[1.0, 3.0], [2.0, 4.0]]) # Only x0 and x2\nX_full = opt.to_all_dim(X_red)\nprint(X_full.shape)\nprint(X_full[:, 1])\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(2, 3)\n[2. 2.]\n```\n:::\n:::\n\n\n### to_red_dim { #spotoptim.SpotOptim.SpotOptim.to_red_dim }\n\n```python\nSpotOptim.SpotOptim.to_red_dim(X_full)\n```\n\nReduce full-dimensional points to optimization space.\nThis method removes fixed dimensions from full-dimensional points,\nextracting only the varying dimensions used in optimization.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------------------|------------|\n| X_full | [ndarray](`ndarray`) | Points in full space, shape (n_samples, n_original_dims). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|-------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Points in reduced space, shape (n_samples, n_reduced_dims). |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#5aa97d49 .cell execution_count=72}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n# Create problem with one fixed dimension\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (2, 2), (-5, 5)], # x1 is fixed at 2\n max_iter=10,\n n_initial=3\n)\nX_full = np.array([[1.0, 2.0, 3.0], [4.0, 2.0, 5.0]])\nX_red = opt.to_red_dim(X_full)\nprint(X_red.shape)\nprint(np.array_equal(X_red, np.array([[1.0, 3.0], [4.0, 5.0]])))\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(2, 2)\nTrue\n```\n:::\n:::\n\n\n### transform_X { #spotoptim.SpotOptim.SpotOptim.transform_X }\n\n```python\nSpotOptim.SpotOptim.transform_X(X)\n```\n\nTransform parameter array from original (natural) to internal scale.\nConverts from natural space (Original) to transformed space (full dimension).\nDoes NOT handle dimension reduction (mapping).\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-------------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Array in Natural Space, shape (n_samples, n_features) | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|---------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Array in Transformed Space (Full Dimension) |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#ee9877f0 .cell execution_count=73}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\nfrom spotoptim.function import sphere\nspot = SpotOptim(fun=sphere, bounds=[(1, 10)], var_trans=['log10'])\nX_orig = np.array([[1], [10], [100]])\nspot.transform_X(X_orig)\n```\n\n::: {.cell-output .cell-output-display execution_count=73}\n```\narray([[0],\n [1],\n [2]])\n```\n:::\n:::\n\n\n### transform_bounds { #spotoptim.SpotOptim.SpotOptim.transform_bounds }\n\n```python\nSpotOptim.SpotOptim.transform_bounds()\n```\n\nTransform bounds from original to internal scale.\nUpdates `self.bounds` (and `self.lower`, `self.upper`) from Natural Space\nto Transformed Space. Calls `transform_value` for each bound and converts\nnumpy types to Python native types (`int` or `float` based on `var_type`).\nHandles also reversed bounds, e.g., as an effect of `reciprocal` transformation.\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Notes {.doc-section .doc-section-notes}\n\nUses settings in `self.var_trans`. It can be one of `id`, `log10`, `log`, `ln`, `sqrt`,\n`exp`, `square`, `cube`, `inv`, `reciprocal`, or `None`. Also supports dynamic\nstrings like `log(x)`, `sqrt(x)`, `pow(x, p)`.\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#40bdd609 .cell execution_count=74}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nimport numpy as np\nspot = SpotOptim(fun=sphere, bounds=[(1, 10), (0.1, 100)])\nspot.var_trans = ['log10', 'sqrt']\nspot.transform_bounds()\nprint(f\"spot.bounds: {spot.bounds}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nspot.bounds: [(0.0, 1.0), (0.31622776601683794, 10.0)]\n```\n:::\n:::\n\n\n### transform_value { #spotoptim.SpotOptim.SpotOptim.transform_value }\n\n```python\nSpotOptim.SpotOptim.transform_value(x, trans)\n```\n\nApply transformation to a single float value.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|-----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|\n| x | [float](`float`) | Value to transform | _required_ |\n| trans | [Optional](`typing.Optional`)\\[[str](`str`)\\] | Transformation name. Can be one of `id`, `log10`, `log`, `ln`, `sqrt`, `exp`, `square`, `cube`, `inv`, `reciprocal`, or `None`. Also supports dynamic strings like `log(x)`, `sqrt(x)`, `pow(x, p)`. | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|------------------|-------------------|\n| | [float](`float`) | Transformed value |\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|--------------------------------------------|\n| | [TypeError](`TypeError`) | If x is not a float. |\n| | [ValueError](`ValueError`) | If an unknown transformation is specified. |\n\n#### Notes {.doc-section .doc-section-notes}\n\nSee also inverse_transform_value.\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#92d4a0ed .cell execution_count=75}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nspot = SpotOptim(fun=sphere, bounds=[(1, 10)])\nspot.transform_value(10, 'log10')\nspot.transform_value(100, 'log(x)')\n```\n\n::: {.cell-output .cell-output-display execution_count=75}\n```\nnp.float64(4.605170185988092)\n```\n:::\n:::\n\n\n### update_repeats_infill_points { #spotoptim.SpotOptim.SpotOptim.update_repeats_infill_points }\n\n```python\nSpotOptim.SpotOptim.update_repeats_infill_points(x_next)\n```\n\nRepeat infill point for noisy function evaluation. Used in the sequential_loop.\nFor noisy objective functions (repeats_surrogate > 1), creates multiple\ncopies of the suggested point for repeated evaluation. Otherwise, returns\nthe point in 2D array format.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|----------------------------------------------|------------|\n| x_next | [ndarray](`ndarray`) | Next point to evaluate, shape (n_features,). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|---------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Points to evaluate, shape (repeats_surrogate, n_features) or (1, n_features) if repeats_surrogate == 1. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#c3d09cef .cell execution_count=76}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere, noisy_sphere\n# Without repeats\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n repeats_surrogate=1\n)\nx_next = np.array([1.0, 2.0])\nx_repeated = opt.update_repeats_infill_points(x_next)\nprint(x_repeated.shape)\n\n# With repeats for noisy function\nopt_noisy = SpotOptim(\n fun=noisy_sphere,\n bounds=[(-5, 5), (-5, 5)],\n repeats_surrogate=3\n)\nx_next = np.array([1.0, 2.0])\nx_repeated = opt_noisy.update_repeats_infill_points(x_next)\nprint(x_repeated.shape)\n# All three copies should be identical\nnp.all(x_repeated[0] == x_repeated[1])\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(1, 2)\n(3, 2)\n```\n:::\n\n::: {.cell-output .cell-output-display execution_count=76}\n```\nnp.True_\n```\n:::\n:::\n\n\n### update_stats { #spotoptim.SpotOptim.SpotOptim.update_stats }\n\n```python\nSpotOptim.SpotOptim.update_stats()\n```\n\nUpdate optimization statistics.\nUpdates various statistics related to the optimization progress:\n * `min_y`: Minimum y value found so far\n * `min_X`: X value corresponding to minimum y\n * `counter`: Total number of function evaluations\n\n#### Notes {.doc-section .doc-section-notes}\n\n`success_rate` is updated separately via `update_success_rate()` method, which is called after each batch of function evaluations.\n\nIf \"noise\" is True (`repeats_initial > 1` or `repeats_surrogate > 1`), additionally computes:\n * `mean_X`: Unique design points (aggregated from repeated evaluations)\n * `mean_y`: Mean y values per design point\n * `var_y`: Variance of y values per design point\n * `min_mean_X`: X value of the best mean y value\n * `min_mean_y`: Best mean y value\n * `min_var_y`: Variance of the best mean y value\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#17d91600 .cell execution_count=77}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n# Without noise\nopt = SpotOptim(fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10, n_initial=5)\nopt.optimize()\nprint(\"SpotOptim stats without noise:\")\nprint(f\"opt.X_: {opt.X_}\")\nprint(f\"opt.y_: {opt.y_}\")\nprint(f\"opt.min_y: {opt.min_y}\")\nprint(f\"opt.min_X: {opt.min_X}\")\nprint(f\"opt.counter: {opt.counter}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSpotOptim stats without noise:\nopt.X_: [[ 4.31661406 -2.98706752]\n [ 0.28684864 3.92905185]\n [ 1.97317349 -3.70660092]\n [-2.41473188 0.16632017]\n [-4.10634486 1.16610501]\n [ 4.37586785 1.41894113]\n [ 2.8253474 3.87008318]\n [-2.41474188 0.16632604]\n [-2.25660285 0.07542405]\n [-1.72444053 -0.28778632]]\nopt.y_: [27.55572928 15.51973059 17.63230402 5.85859245 18.22186903 21.16161338\n 22.96013178 5.85864268 5.09794521 3.05651611]\nopt.min_y: 3.056516107962276\nopt.min_X: [-1.72444053 -0.28778632]\nopt.counter: 10\n```\n:::\n:::\n\n\n::: {#db82ef81 .cell execution_count=78}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import noisy_sphere\n# With noise\nopt_noise = SpotOptim(fun=noisy_sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n repeats_surrogate=2,\n repeats_initial=2)\nopt_noise.optimize()\nprint(\"SpotOptim stats with noise:\")\nprint(f\"opt_noise.X_: {opt_noise.X_}\")\nprint(f\"opt_noise.y_: {opt_noise.y_}\")\nprint(f\"opt_noise.min_y: {opt_noise.min_y}\")\nprint(f\"opt_noise.min_X: {opt_noise.min_X}\")\nprint(f\"opt_noise.counter: {opt_noise.counter}\")\nprint(f\"opt_noise.mean_X: {opt_noise.mean_X}\")\nprint(f\"opt_noise.mean_y: {opt_noise.mean_y}\")\nprint(f\"opt_noise.var_y: {opt_noise.var_y}\")\nprint(f\"opt_noise.min_mean_X: {opt_noise.min_mean_X}\")\nprint(f\"opt_noise.min_mean_y: {opt_noise.min_mean_y}\")\nprint(f\"opt_noise.min_var_y: {opt_noise.min_var_y}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSpotOptim stats with noise:\nopt_noise.X_: [[-4.34670042 -4.29344041]\n [-4.34670042 -4.29344041]\n [ 0.59785002 1.78569096]\n [ 0.59785002 1.78569096]\n [ 1.37986219 -1.61587239]\n [ 1.37986219 -1.61587239]\n [ 3.30811094 4.61751022]\n [ 3.30811094 4.61751022]\n [-2.76668523 0.9968025 ]\n [-2.76668523 0.9968025 ]\n [ 0.34674766 1.08969718]\n [ 0.34674766 1.08969718]\n [ 0.29133996 0.60430193]\n [ 0.29133996 0.60430193]\n [ 0.379487 0.07398903]\n [ 0.379487 0.07398903]\n [ 0.24675965 0.11214783]\n [ 0.24675965 0.11214783]\n [ 0.43014284 0.12837287]\n [ 0.43014284 0.12837287]]\nopt_noise.y_: [ 3.72962376e+01 3.72434061e+01 3.44543366e+00 3.71427451e+00\n 4.43583457e+00 4.46190265e+00 3.23015835e+01 3.23947812e+01\n 8.69627390e+00 8.92409790e+00 1.43441505e+00 1.35762371e+00\n 3.91136895e-01 4.05213293e-01 1.04608058e-01 -2.49340162e-02\n 1.58520530e-01 -1.04449578e-02 1.04385392e-01 3.44134144e-01]\nopt_noise.min_y: -0.024934016181757196\nopt_noise.min_X: [0.379487 0.07398903]\nopt_noise.counter: 20\nopt_noise.mean_X: [[-4.34670042 -4.29344041]\n [-2.76668523 0.9968025 ]\n [ 0.24675965 0.11214783]\n [ 0.29133996 0.60430193]\n [ 0.34674766 1.08969718]\n [ 0.379487 0.07398903]\n [ 0.43014284 0.12837287]\n [ 0.59785002 1.78569096]\n [ 1.37986219 -1.61587239]\n [ 3.30811094 4.61751022]]\nopt_noise.mean_y: [37.26982184 8.8101859 0.07403779 0.39817509 1.39601938 0.03983702\n 0.22425977 3.57985409 4.44886861 32.34818235]\nopt_noise.var_y: [6.97789966e-04 1.29759436e-02 7.13733398e-03 4.95362454e-05\n 1.47422756e-03 4.19528726e-03 1.43698661e-02 1.80688502e-02\n 1.69886138e-04 2.17145039e-03]\nopt_noise.min_mean_X: [0.379487 0.07398903]\nopt_noise.min_mean_y: 0.03983702099066562\nopt_noise.min_var_y: 0.004195287256391378\n```\n:::\n:::\n\n\n### update_storage { #spotoptim.SpotOptim.SpotOptim.update_storage }\n\n```python\nSpotOptim.SpotOptim.update_storage(X_new, y_new)\n```\n\nUpdate storage (`X_`, `y_`) with new evaluation points.\nAppends new design points and their function values to the storage arrays.\nPoints are converted from internal scale to original scale before storage.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------------------------|------------|\n| X_new | [ndarray](`ndarray`) | New design points in internal scale, shape (n_new, n_features). | _required_ |\n| y_new | [ndarray](`ndarray`) | Function values at X_new, shape (n_new,). | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#36a79315 .cell execution_count=79}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5)\n# Initialize with some data\nopt.X_ = np.array([[1, 2], [3, 4]])\nopt.y_ = np.array([5.0, 25.0])\nprint(\"Initial storage:\")\nprint(opt.X_)\nprint(opt.y_)\n# Add new points\nX_new = np.array([[0, 1], [2, 3]])\ny_new = np.array([1.0, 13.0])\nopt.update_storage(X_new, y_new)\nprint(\"Updated storage:\")\nprint(opt.X_)\nprint(opt.y_)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nInitial storage:\n[[1 2]\n [3 4]]\n[ 5. 25.]\nUpdated storage:\n[[1. 2.]\n [3. 4.]\n [0. 1.]\n [2. 3.]]\n[ 5. 25. 1. 13.]\n```\n:::\n:::\n\n\n### update_success_rate { #spotoptim.SpotOptim.SpotOptim.update_success_rate }\n\n```python\nSpotOptim.SpotOptim.update_success_rate(y_new)\n```\n\nUpdate the rolling success rate of the optimization process.\nA success is counted only if the new value is better (smaller) than the best\nfound y value so far. The success rate is calculated based on the last\n`window_size` successes.\nImportant: This method should be called BEFORE updating self.y_ to correctly\ntrack improvements against the previous best value.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|------------------------------------------------------------------|------------|\n| y_new | [ndarray](`ndarray`) | The new function values to consider for the success rate update. | _required_ |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#f21a2f16 .cell execution_count=80}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10, n_initial=5)\nprint(opt.success_rate)\nopt.X_ = np.array([[1, 2], [3, 4], [0, 1]])\nopt.y_ = np.array([5.0, 3.0, 2.0])\nopt.update_success_rate(np.array([1.5, 2.5]))\nprint(opt.success_rate)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n0.0\n0.5\n```\n:::\n:::\n\n\n### validate_x0 { #spotoptim.SpotOptim.SpotOptim.validate_x0 }\n\n```python\nSpotOptim.SpotOptim.validate_x0(x0)\n```\n\nValidate and process starting point x0. Called in `__init__` and `optimize`.\nThis method checks that x0:\n * Is a numpy array\n * Has the correct number of dimensions\n * Has values within bounds (in original scale)\n * Is properly transformed to internal scale\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|-----------------------------------|----------------------------------|------------|\n| x0 | [array](`array`) - [like](`like`) | Starting point in original scale | _required_ |\n\n#### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|---------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Validated and transformed x0 in internal scale, shape (n_features,) |\n\n#### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|------------------|\n| | [ValueError](`ValueError`) | If x0 is invalid |\n\n#### Examples {.doc-section .doc-section-examples}\n\n::: {#80a0f6a1 .cell execution_count=81}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (5,5), (-10, 10)],\n x0=np.array([1.0, 5.0, 9.0]),\n var_trans=[\"log10\", \"id\", \"sqrt\"]\n)\n# x0 is validated during initialization and transformed to internal scale\nprint(f\"x0 in internal scale: {opt.x0}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nx0 in internal scale: [0. 3.]\n```\n:::\n:::\n\n\n", "supporting": [ - "SpotOptim.SpotOptim_files" + "SpotOptim.SpotOptim_files/figure-html" ], "filters": [], "includes": {} diff --git a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-3-output-1.png b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-3-output-1.png index 3bb8719b..0f3cf1d9 100644 Binary files a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-3-output-1.png and b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-3-output-1.png differ diff --git a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-33-output-2.png b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-33-output-2.png index 08bd0b59..26e88479 100644 Binary files a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-33-output-2.png and b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-33-output-2.png differ diff --git a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-33-output-3.png b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-33-output-3.png index 08bd0b59..26e88479 100644 Binary files a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-33-output-3.png and b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-33-output-3.png differ diff --git a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-34-output-2.png b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-34-output-2.png index 3dc1c9c4..26ffb3c4 100644 Binary files a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-34-output-2.png and b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-34-output-2.png differ diff --git a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-34-output-3.png b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-34-output-3.png index 3dc1c9c4..26ffb3c4 100644 Binary files a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-34-output-3.png and b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-34-output-3.png differ diff --git a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-55-output-1.png b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-55-output-1.png index 7cd2eddd..b7ba1569 100644 Binary files a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-55-output-1.png and b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-55-output-1.png differ diff --git a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-56-output-2.png b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-56-output-2.png new file mode 100644 index 00000000..a192c78f Binary files /dev/null and b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-56-output-2.png differ diff --git a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-57-output-1.png b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-57-output-1.png index 8c731d14..705520b3 100644 Binary files a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-57-output-1.png and b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-57-output-1.png differ diff --git a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-57-output-2.png b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-57-output-2.png index 4b31402b..c7d1b0a5 100644 Binary files a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-57-output-2.png and b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-57-output-2.png differ diff --git a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-58-output-1.png b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-58-output-1.png index 52aed1a5..3fcdfc7a 100644 Binary files a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-58-output-1.png and b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-58-output-1.png differ diff --git a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-59-output-1.png b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-59-output-1.png index 5f2210cd..a192c78f 100644 Binary files a/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-59-output-1.png and b/_freeze/docs/reference/SpotOptim.SpotOptim/figure-html/cell-59-output-1.png differ diff --git a/_freeze/docs/reference/SpotOptim.SpotOptimConfig/execute-results/html.json b/_freeze/docs/reference/SpotOptim.SpotOptimConfig/execute-results/html.json index 98e2bfe2..54033347 100644 --- a/_freeze/docs/reference/SpotOptim.SpotOptimConfig/execute-results/html.json +++ b/_freeze/docs/reference/SpotOptim.SpotOptimConfig/execute-results/html.json @@ -1,10 +1,10 @@ { - "hash": "e6ecfdc4a2aacbb22d5c8e84063f97e1", + "hash": "9b1327a8a7ad5461d01d3263e80b29e3", "result": { "engine": "jupyter", - "markdown": "---\ntitle: SpotOptim.SpotOptimConfig\n---\n\n\n\n```python\nSpotOptim.SpotOptimConfig(\n bounds=None,\n max_iter=20,\n n_initial=10,\n surrogate=None,\n acquisition='y',\n var_type=None,\n var_name=None,\n var_trans=None,\n tolerance_x=None,\n max_time=np.inf,\n repeats_initial=1,\n repeats_surrogate=1,\n ocba_delta=0,\n tensorboard_log=False,\n tensorboard_path=None,\n tensorboard_clean=False,\n fun_mo2so=None,\n seed=None,\n verbose=False,\n warnings_filter='ignore',\n n_infill_points=1,\n max_surrogate_points=None,\n selection_method='distant',\n acquisition_failure_strategy='random',\n penalty=False,\n penalty_val=None,\n acquisition_fun_return_size=3,\n acquisition_optimizer='differential_evolution',\n restart_after_n=100,\n restart_inject_best=True,\n x0=None,\n de_x0_prob=0.1,\n tricands_fringe=False,\n prob_de_tricands=0.8,\n window_size=None,\n min_tol_metric='chebyshev',\n prob_surrogate=None,\n n_jobs=1,\n eval_batch_size=1,\n acquisition_optimizer_kwargs=None,\n args=(),\n kwargs=None,\n)\n```\n\nConfiguration parameters for SpotOptim.\n\n## Attributes {.doc-section .doc-section-attributes}\n\n| Name | Type | Description |\n|------------------------------|-----------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| bounds | [Optional](`typing.Optional`)\\[[list](`list`)\\] | Bounds for the input variables. |\n| max_iter | [int](`int`) | Maximum number of iterations. |\n| n_initial | [int](`int`) | Number of initial points. |\n| surrogate | [Optional](`typing.Optional`)\\[[object](`object`)\\] | Surrogate model. |\n| acquisition | [str](`str`) | Acquisition function. |\n| var_type | [Optional](`typing.Optional`)\\[[list](`list`)\\] | Type of variables. |\n| var_name | [Optional](`typing.Optional`)\\[[list](`list`)\\] | Name of variables. |\n| var_trans | [Optional](`typing.Optional`)\\[[list](`list`)\\] | Transformation of variables. |\n| tolerance_x | [Optional](`typing.Optional`)\\[[float](`float`)\\] | Tolerance for input variables. |\n| max_time | [float](`float`) | Maximum time. |\n| repeats_initial | [int](`int`) | Number of repeats for initial points. |\n| repeats_surrogate | [int](`int`) | Number of repeats for surrogate points. |\n| ocba_delta | [int](`int`) | Delta for OCBA. |\n| tensorboard_log | [bool](`bool`) | Whether to log to TensorBoard. |\n| tensorboard_path | [Optional](`typing.Optional`)\\[[str](`str`)\\] | Path to TensorBoard logs. |\n| tensorboard_clean | [bool](`bool`) | Whether to clean TensorBoard logs. |\n| fun_mo2so | [Optional](`typing.Optional`)\\[[Callable](`typing.Callable`)\\] | Function to convert multi-objective to single-objective. |\n| seed | [Optional](`typing.Optional`)\\[[int](`int`)\\] | Seed for random number generator. |\n| verbose | [bool](`bool`) | Whether to print verbose output. |\n| warnings_filter | [Literal](`typing.Literal`)\\[\\'default\\', \\'error\\', \\'ignore\\'\\] | Filter for warnings. |\n| n_infill_points | [int](`int`) | Number of infill points. |\n| max_surrogate_points | [Optional](`typing.Optional`)\\[[Union](`typing.Union`)\\[[int](`int`), [List](`typing.List`)\\[[int](`int`)\\]\\]\\] | Maximum number of surrogate points. |\n| selection_method | [str](`str`) | Method for selecting infill points. |\n| acquisition_failure_strategy | [str](`str`) | Strategy for handling acquisition function failures. |\n| penalty | [bool](`bool`) | Whether to use penalty. |\n| penalty_val | [Optional](`typing.Optional`)\\[[float](`float`)\\] | Penalty value. |\n| acquisition_fun_return_size | [int](`int`) | Size of the acquisition function return. |\n| acquisition_optimizer | [Union](`typing.Union`)\\[[str](`str`), [Callable](`typing.Callable`)\\] | Optimizer for the acquisition function. |\n| restart_after_n | [int](`int`) | Number of iterations after which to restart. |\n| restart_inject_best | [bool](`bool`) | Whether to inject the best point after restart. |\n| x0 | [Optional](`typing.Optional`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`)\\] | Initial guess for the input variables. |\n| de_x0_prob | [float](`float`) | Probability of using differential evolution for initial guess. |\n| tricands_fringe | [bool](`bool`) | Whether to use fringe for tricands. |\n| prob_de_tricands | [float](`float`) | Probability of using tricands for differential evolution. |\n| window_size | [Optional](`typing.Optional`)\\[[int](`int`)\\] | Size of the window for tricands. |\n| min_tol_metric | [str](`str`) | Metric for minimum tolerance. |\n| prob_surrogate | [Optional](`typing.Optional`)\\[[List](`typing.List`)\\[[float](`float`)\\]\\] | Probability of using surrogate. |\n| n_jobs | [int](`int`) | Number of parallel workers. ``1`` runs sequentially. Values ``> 1`` activate steady-state parallel optimization. On standard GIL builds a hybrid executor is used: ``ProcessPoolExecutor`` for objective evaluations (process isolation; supports lambdas and closures via ``dill``) and ``ThreadPoolExecutor`` for surrogate search tasks (shared heap; zero serialization overhead). On free-threaded Python builds (``python3.13t``, ``--disable-gil``), both pools are ``ThreadPoolExecutor`` instances, achieving true CPU-level parallelism without ``dill`` for eval tasks. ``-1`` resolves to ``os.cpu_count()`` (all available CPU cores). ``0`` and values ``< -1`` raise ``ValueError``. Defaults to ``1``. |\n| eval_batch_size | [int](`int`) | Number of candidate points to accumulate before dispatching a single ``fun(X_batch)`` call to the process pool. ``1`` (default) dispatches each candidate immediately, preserving current behavior. Values ``> 1`` reduce process-spawn and IPC overhead when ``fun`` supports vectorized batch input. Must be ``>= 1``. Defaults to ``1``. |\n| acquisition_optimizer_kwargs | [Optional](`typing.Optional`)\\[[Dict](`typing.Dict`)\\[[str](`str`), [Any](`typing.Any`)\\]\\] | Keyword arguments for the acquisition function optimizer. |\n| args | [Tuple](`typing.Tuple`) | Arguments for the objective function. |\n| kwargs | [Optional](`typing.Optional`)\\[[Dict](`typing.Dict`)\\[[str](`str`), [Any](`typing.Any`)\\]\\] | Keyword arguments for the objective function. |\n\n## Examples {.doc-section .doc-section-examples}\n\n\n::: {#971a9018 .cell execution_count=1}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim.SpotOptim import SpotOptim\nfrom sklearn.gaussian_process import GaussianProcessRegressor\nfrom spotoptim.SpotOptim import SpotOptimConfig\nfrom sklearn.gaussian_process.kernels import Matern, ConstantKernel\n\nkernel = ConstantKernel(1.0, (1e-2, 1e12)) * Matern(length_scale=1.0, length_scale_bounds=(1e-4, 1e2), nu=2.5)\nsurrogate = GaussianProcessRegressor(kernel=kernel, n_restarts_optimizer=100)\n\nconfig = SpotOptimConfig(\n bounds=[(0, 1), (0, 1)],\n max_iter=20,\n n_initial=10,\n surrogate=surrogate,\n acquisition=\"y\",\n var_type=[\"continuous\", \"continuous\"],\n var_name=[\"x1\", \"x2\"],\n var_trans=[\"identity\", \"identity\"],\n tolerance_x=1e-6,\n max_time=np.inf,\n repeats_initial=1,\n repeats_surrogate=1,\n ocba_delta=0,\n tensorboard_log=False,\n tensorboard_path=None,\n tensorboard_clean=False,\n fun_mo2so=None,\n seed=None,\n verbose=False,\n warnings_filter=\"ignore\",\n n_infill_points=1,\n max_surrogate_points=None,\n selection_method=\"distant\",\n acquisition_failure_strategy=\"random\",\n penalty=False,\n penalty_val=None,\n acquisition_fun_return_size=3,\n acquisition_optimizer=\"differential_evolution\",\n restart_after_n=100,\n restart_inject_best=True,\n x0=None,\n de_x0_prob=0.1,\n tricands_fringe=False,\n prob_de_tricands=0.8,\n window_size=None,\n min_tol_metric=\"chebyshev\",\n prob_surrogate=None,\n n_jobs=1,\n acquisition_optimizer_kwargs=None,\n args=(),\n kwargs=None,\n)\n```\n:::\n\n\n", + "markdown": "---\ntitle: SpotOptim.SpotOptimConfig\n---\n\n\n\n```python\nSpotOptim.SpotOptimConfig(\n bounds=None,\n max_iter=20,\n n_initial=10,\n surrogate=None,\n acquisition='y',\n var_type=None,\n var_name=None,\n var_trans=None,\n tolerance_x=None,\n max_time=np.inf,\n repeats_initial=1,\n repeats_surrogate=1,\n ocba_delta=0,\n tensorboard_log=False,\n tensorboard_path=None,\n tensorboard_clean=False,\n fun_mo2so=None,\n seed=None,\n verbose=False,\n warnings_filter='ignore',\n n_infill_points=1,\n max_surrogate_points=None,\n selection_method='distant',\n acquisition_failure_strategy='random',\n penalty=False,\n penalty_val=None,\n acquisition_fun_return_size=3,\n acquisition_optimizer='differential_evolution',\n restart_after_n=100,\n restart_inject_best=True,\n max_restarts=None,\n x0=None,\n de_x0_prob=0.1,\n tricands_fringe=False,\n prob_de_tricands=0.8,\n window_size=None,\n min_tol_metric='chebyshev',\n prob_surrogate=None,\n acquisition_optimizer_kwargs=None,\n args=(),\n kwargs=None,\n)\n```\n\nConfiguration parameters for SpotOptim.\n\n## Attributes {.doc-section .doc-section-attributes}\n\n| Name | Type | Description |\n|------------------------------|-----------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| bounds | [Optional](`typing.Optional`)\\[[list](`list`)\\] | Bounds for the input variables. |\n| max_iter | [int](`int`) | Maximum number of iterations. |\n| n_initial | [int](`int`) | Number of initial points. |\n| surrogate | [Optional](`typing.Optional`)\\[[object](`object`)\\] | Surrogate model. |\n| acquisition | [str](`str`) | Acquisition function. |\n| var_type | [Optional](`typing.Optional`)\\[[list](`list`)\\] | Type of variables. |\n| var_name | [Optional](`typing.Optional`)\\[[list](`list`)\\] | Name of variables. |\n| var_trans | [Optional](`typing.Optional`)\\[[list](`list`)\\] | Transformation of variables. |\n| tolerance_x | [Optional](`typing.Optional`)\\[[float](`float`)\\] | Tolerance for input variables. |\n| max_time | [float](`float`) | Maximum time. |\n| repeats_initial | [int](`int`) | Number of repeats for initial points. |\n| repeats_surrogate | [int](`int`) | Number of repeats for surrogate points. |\n| ocba_delta | [int](`int`) | Delta for OCBA. |\n| tensorboard_log | [bool](`bool`) | Whether to log to TensorBoard. |\n| tensorboard_path | [Optional](`typing.Optional`)\\[[str](`str`)\\] | Path to TensorBoard logs. |\n| tensorboard_clean | [bool](`bool`) | Whether to clean old TensorBoard logs (the configured tensorboard_path, or the 'runs' folder if no path is set). |\n| fun_mo2so | [Optional](`typing.Optional`)\\[[Callable](`typing.Callable`)\\] | Function to convert multi-objective to single-objective. |\n| seed | [Optional](`typing.Optional`)\\[[int](`int`)\\] | Seed for random number generator. |\n| verbose | [bool](`bool`) | Whether to print verbose output. |\n| warnings_filter | [Literal](`typing.Literal`)\\[\\'default\\', \\'error\\', \\'ignore\\'\\] | Filter for warnings. |\n| n_infill_points | [int](`int`) | Number of infill points. |\n| max_surrogate_points | [Optional](`typing.Optional`)\\[[Union](`typing.Union`)\\[[int](`int`), [List](`typing.List`)\\[[int](`int`)\\]\\]\\] | Maximum number of surrogate points. |\n| selection_method | [str](`str`) | Method for selecting infill points. |\n| acquisition_failure_strategy | [str](`str`) | Strategy for handling acquisition function failures. |\n| penalty | [bool](`bool`) | Whether to use penalty. |\n| penalty_val | [Optional](`typing.Optional`)\\[[float](`float`)\\] | Penalty value. |\n| acquisition_fun_return_size | [int](`int`) | Size of the acquisition function return. |\n| acquisition_optimizer | [Union](`typing.Union`)\\[[str](`str`), [Callable](`typing.Callable`)\\] | Optimizer for the acquisition function. |\n| restart_after_n | [int](`int`) | Number of iterations after which to restart. |\n| restart_inject_best | [bool](`bool`) | Whether to inject the best point after restart. |\n| max_restarts | [Optional](`typing.Optional`)\\[[int](`int`)\\] | Patience-based early-stopping. When set, the optimizer terminates after this many consecutive restarts that fail to improve the best objective value. ``None`` (default) disables the check and preserves legacy behavior. |\n| x0 | [Optional](`typing.Optional`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`)\\] | Initial guess for the input variables. |\n| de_x0_prob | [float](`float`) | Probability of using differential evolution for initial guess. |\n| tricands_fringe | [bool](`bool`) | Whether to use fringe for tricands. |\n| prob_de_tricands | [float](`float`) | Probability of using tricands for differential evolution. |\n| window_size | [Optional](`typing.Optional`)\\[[int](`int`)\\] | Size of the window for tricands. |\n| min_tol_metric | [str](`str`) | Metric for minimum tolerance. |\n| prob_surrogate | [Optional](`typing.Optional`)\\[[List](`typing.List`)\\[[float](`float`)\\]\\] | Probability of using surrogate. |\n| acquisition_optimizer_kwargs | [Optional](`typing.Optional`)\\[[Dict](`typing.Dict`)\\[[str](`str`), [Any](`typing.Any`)\\]\\] | Keyword arguments for the acquisition function optimizer. |\n| args | [Tuple](`typing.Tuple`) | Arguments for the objective function. |\n| kwargs | [Optional](`typing.Optional`)\\[[Dict](`typing.Dict`)\\[[str](`str`), [Any](`typing.Any`)\\]\\] | Keyword arguments for the objective function. |\n\n## Examples {.doc-section .doc-section-examples}\n\n\n::: {#95e7b401 .cell execution_count=1}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim.SpotOptim import SpotOptim\nfrom sklearn.gaussian_process import GaussianProcessRegressor\nfrom spotoptim.SpotOptim import SpotOptimConfig\nfrom sklearn.gaussian_process.kernels import Matern, ConstantKernel\n\nkernel = ConstantKernel(1.0, (1e-2, 1e12)) * Matern(length_scale=1.0, length_scale_bounds=(1e-4, 1e2), nu=2.5)\nsurrogate = GaussianProcessRegressor(kernel=kernel, n_restarts_optimizer=100)\n\nconfig = SpotOptimConfig(\n bounds=[(0, 1), (0, 1)],\n max_iter=20,\n n_initial=10,\n surrogate=surrogate,\n acquisition=\"y\",\n var_type=[\"continuous\", \"continuous\"],\n var_name=[\"x1\", \"x2\"],\n var_trans=[\"identity\", \"identity\"],\n tolerance_x=1e-6,\n max_time=np.inf,\n repeats_initial=1,\n repeats_surrogate=1,\n ocba_delta=0,\n tensorboard_log=False,\n tensorboard_path=None,\n tensorboard_clean=False,\n fun_mo2so=None,\n seed=None,\n verbose=False,\n warnings_filter=\"ignore\",\n n_infill_points=1,\n max_surrogate_points=None,\n selection_method=\"distant\",\n acquisition_failure_strategy=\"random\",\n penalty=False,\n penalty_val=None,\n acquisition_fun_return_size=3,\n acquisition_optimizer=\"differential_evolution\",\n restart_after_n=100,\n restart_inject_best=True,\n max_restarts=None,\n x0=None,\n de_x0_prob=0.1,\n tricands_fringe=False,\n prob_de_tricands=0.8,\n window_size=None,\n min_tol_metric=\"chebyshev\",\n prob_surrogate=None,\n acquisition_optimizer_kwargs=None,\n args=(),\n kwargs=None,\n)\n```\n:::\n\n\n", "supporting": [ - "SpotOptim.SpotOptimConfig_files" + "SpotOptim.SpotOptimConfig_files/figure-html" ], "filters": [], "includes": {} diff --git a/_freeze/docs/reference/SpotOptim/execute-results/html.json b/_freeze/docs/reference/SpotOptim/execute-results/html.json deleted file mode 100644 index 8a134f98..00000000 --- a/_freeze/docs/reference/SpotOptim/execute-results/html.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "hash": "35eb59900a0fc96fc2bb46d892ef7d9e", - "result": { - "engine": "jupyter", - "markdown": "---\ntitle: SpotOptim\n---\n\n\n\n`SpotOptim`\n\n\n\n## Classes\n\n| Name | Description |\n| --- | --- |\n| [SpotOptim](#spotoptim.SpotOptim.SpotOptim) | SPOT optimizer compatible with scipy.optimize interface. |\n| [SpotOptimConfig](#spotoptim.SpotOptim.SpotOptimConfig) | Configuration parameters for SpotOptim. |\n| [SpotOptimState](#spotoptim.SpotOptim.SpotOptimState) | Mutable state of the optimization process. |\n\n### SpotOptim { #spotoptim.SpotOptim.SpotOptim }\n\n```python\nSpotOptim.SpotOptim(\n fun,\n bounds=None,\n max_iter=20,\n n_initial=10,\n surrogate=None,\n acquisition='y',\n var_type=None,\n var_name=None,\n var_trans=None,\n tolerance_x=None,\n max_time=np.inf,\n repeats_initial=1,\n repeats_surrogate=1,\n ocba_delta=0,\n tensorboard_log=False,\n tensorboard_path=None,\n tensorboard_clean=False,\n fun_mo2so=None,\n seed=None,\n verbose=False,\n warnings_filter='ignore',\n n_infill_points=1,\n max_surrogate_points=None,\n selection_method='distant',\n acquisition_failure_strategy='random',\n penalty=False,\n penalty_val=None,\n acquisition_fun_return_size=3,\n acquisition_optimizer='differential_evolution',\n restart_after_n=100,\n restart_inject_best=True,\n x0=None,\n de_x0_prob=0.1,\n tricands_fringe=False,\n prob_de_tricands=0.8,\n window_size=None,\n min_tol_metric='chebyshev',\n prob_surrogate=None,\n n_jobs=1,\n acquisition_optimizer_kwargs=None,\n args=(),\n kwargs=None,\n)\n```\n\nSPOT optimizer compatible with scipy.optimize interface.\n\n#### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|------------------------------|----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------|\n| fun | [callable](`callable`) | Objective function to minimize. Should accept array of shape (n_samples, n_features). | _required_ |\n| bounds | list of tuple | Bounds for each dimension as [(low, high), ...]. | `None` |\n| max_iter | [int](`int`) | Maximum number of total function evaluations (including initial design). For example, max_iter=30 with n_initial=10 will perform 10 initial evaluations plus 20 sequential optimization iterations. Defaults to 20. | `20` |\n| n_initial | [int](`int`) | Number of initial design points. Defaults to 10. | `10` |\n| surrogate | [object](`object`) | Surrogate model with scikit-learn interface (fit/predict methods). If None, uses a Gaussian Process Regressor with Matern kernel. Default configuration:: from sklearn.gaussian_process import GaussianProcessRegressor from sklearn.gaussian_process.kernels import Matern, ConstantKernel kernel = ConstantKernel(1.0, (1e-2, 1e12)) * Matern(length_scale=1.0, length_scale_bounds=(1e-4, 1e2), nu=2.5) surrogate = GaussianProcessRegressor(kernel=kernel, n_restarts_optimizer=100) surrogate = GaussianProcessRegressor( kernel=kernel, n_restarts_optimizer=10, normalize_y=True, random_state=self.seed, ) Alternative surrogates can be provided, including SpotOptim's Kriging model, Random Forests, or any scikit-learn compatible regressor. See Examples section. Defaults to None (uses default Gaussian Process configuration). | `None` |\n| acquisition | [str](`str`) | Acquisition function ('ei', 'y', 'pi'). Defaults to 'y'. | `'y'` |\n| var_type | list of str | Variable types for each dimension. Supported types: - 'float': Python floats, continuous optimization (no rounding) - 'int': Python int, float values will be rounded to integers - 'factor': Unordered categorical data, internally mapped to int values (e.g., \"red\"->0, \"green\"->1, etc.) Defaults to None (which sets all dimensions to 'float'). | `None` |\n| var_name | list of str | Variable names for each dimension. If None, uses default names ['x0', 'x1', 'x2', ...]. Defaults to None. | `None` |\n| tolerance_x | [float](`float`) | Minimum distance between points. Defaults to np.sqrt(np.spacing(1)) | `None` |\n| var_trans | list of str | Variable transformations for each dimension. Supported: - 'log': Logarithmic transformation, e.g. \"log10\" for base-10 log - 'sqrt': Square root transformation, \"sqrt\" for square root - None or 'id' or 'None': No transformation Defaults to None (no transformations). | `None` |\n| max_time | [float](`float`) | Maximum runtime in minutes. If np.inf (default), no time limit. The optimization terminates when either max_iter evaluations are reached OR max_time minutes have elapsed, whichever comes first. Defaults to np.inf. | `np.inf` |\n| repeats_initial | [int](`int`) | Number of times to evaluate each initial design point. Useful for noisy objective functions. If > 1, noise handling is activated and statistics (mean, variance) are tracked. Defaults to 1. | `1` |\n| repeats_surrogate | [int](`int`) | Number of times to evaluate each surrogate-suggested point. Useful for noisy objective functions. If > 1, noise handling is activated and statistics (mean, variance) are tracked. Defaults to 1. | `1` |\n| ocba_delta | [int](`int`) | Number of additional evaluations to allocate using Optimal Computing Budget Allocation (OCBA) when noise handling is active. OCBA determines which existing design points should be re-evaluated to best distinguish between alternatives. Only used when repeats_surrogate > 1 and ocba_delta > 0. Requires at least 3 design points with variance information. Defaults to 0 (no OCBA). | `0` |\n| tensorboard_log | [bool](`bool`) | Enable TensorBoard logging. If True, optimization metrics and hyperparameters are logged to TensorBoard. View logs by running: `tensorboard --logdir=` in a separate terminal. Defaults to False. | `False` |\n| tensorboard_path | [str](`str`) | Path for TensorBoard log files. If None and tensorboard_log is True, creates a default path: runs/spotoptim_YYYYMMDD_HHMMSS. Defaults to None. | `None` |\n| tensorboard_clean | [bool](`bool`) | If True, removes all old TensorBoard log directories from the 'runs' folder before starting optimization. Use with caution as this permanently deletes all subdirectories in 'runs'. Defaults to False. | `False` |\n| fun_mo2so | [callable](`callable`) | Function to convert multi-objective values to single-objective. Takes an array of shape (n_samples, n_objectives) and returns array of shape (n_samples,). If None and objective function returns multi-objective values, uses first objective. Defaults to None. | `None` |\n| seed | [int](`int`) | Random seed for reproducibility. Defaults to None. | `None` |\n| verbose | [bool](`bool`) | Print progress information. Defaults to False. | `False` |\n| warnings_filter | [str](`str`) | Filter for warnings. One of \"error\", \"ignore\", \"always\", \"all\", \"default\", \"module\", or \"once\". Defaults to \"ignore\". | `'ignore'` |\n| n_infill_points | [int](`int`) | Number of infill points to suggest at each iteration. Defaults to 1. If > 1, multiple distinct points are proposed using the optimizer and fallback strategies. | `1` |\n| max_surrogate_points | [int](`int`) | Maximum number of points to use for surrogate model fitting. If None, all points are used. If the number of evaluated points exceeds this limit, a subset is selected using the selection method. Defaults to None. | `None` |\n| selection_method | [str](`str`) | Method for selecting points when max_surrogate_points is exceeded. Options: 'distant' (Select points that are distant from each other via K-means clustering) or 'best' (Select all points from the cluster with the best mean objective value). Defaults to 'distant'. | `'distant'` |\n| acquisition_failure_strategy | [str](`str`) | Strategy for handling acquisition function failures. Options: 'random' (space-filling design via Latin Hypercube Sampling) Defaults to 'random'. | `'random'` |\n| penalty | [bool](`bool`) | Whether to use penalty for handling NaN/inf values in objective function evaluations. Defaults to False. | `False` |\n| penalty_val | [float](`float`) | Penalty value to replace NaN/inf values in objective function evaluations. When the objective function returns NaN or inf, these values are replaced with penalty plus a small random noise (sampled from N(0, 0.1)) to avoid identical penalty values. This allows optimization to continue despite occasional function evaluation failures. Defaults to None. | `None` |\n| acquisition_fun_return_size | [int](`int`) | Number of top candidates to return from acquisition function optimization. Defaults to 3. | `3` |\n| acquisition_optimizer | [str](`str`) or [callable](`callable`) | Optimizer to use for maximizing acquisition function. Can be \"differential_evolution\" (default) or any method name supported by scipy.optimize.minimize (e.g., \"Nelder-Mead\", \"L-BFGS-B\"). Can also be a callable with signature compatible with scipy.optimize.minimize (fun, x0, bounds, ...). A specific version is \"de_tricands\", which combines DE with Tricands. It can be parameterized with \"prob_de_tricands\" (probability of using DE). Defaults to \"differential_evolution\". | `'differential_evolution'` |\n| acquisition_optimizer_kwargs | [dict](`dict`) | Kwargs passed to the acquisition function optimizer and GPR surrogate optimizer. Defaults to {'maxiter': 10000, 'gtol': 1e-9}. | `None` |\n| restart_after_n | [int](`int`) | Number of consecutive iterations with zero success rate before triggering a restart. Defaults to 100. | `100` |\n| restart_inject_best | [bool](`bool`) | Whether to inject the best solution found so far as a starting point for the next restart. Defaults to True. | `True` |\n| x0 | [array](`array`) - [like](`like`) | Starting point for optimization, shape (n_features,). If provided, this point will be evaluated first and included in the initial design. The point should be within the bounds and will be validated before use. Defaults to None (no starting point, uses only LHS design). | `None` |\n| de_x0_prob | [float](`float`) | Probability of using the best point as starting point for differential evolution. Defaults to 0.1. | `0.1` |\n| tricands_fringe | [bool](`bool`) | Whether to use the fringe of the design space for the initial design. Defaults to False. | `False` |\n| prob_de_tricands | [float](`float`) | Probability of using differential evolution as an optimizer on the surrogate model. 1 - prob_de_tricands is the probability of using tricands. Defaults to 0.8. | `0.8` |\n| window_size | [int](`int`) | Window size for success rate calculation. | `None` |\n| min_tol_metric | [str](`str`) | Distance metric used when checking `tolerance_x` for duplicate detection. Default is \"chebyshev\". Supports all metrics from scipy.spatial.distance.cdist, including: - \"chebyshev\": L-infinity distance (hypercube). Default. Matches previous behavior. - \"euclidean\": L2 distance (hypersphere). - \"minkowski\": Lp distance (default p=2). - \"cityblock\": Manhattan/L1 distance. - \"cosine\": Cosine distance. - \"correlation\": Correlation distance. - \"canberra\", \"braycurtis\", \"sqeuclidean\", etc. | `'chebyshev'` |\n\n#### Attributes {.doc-section .doc-section-attributes}\n\n| Name | Type | Description |\n|------------------------------|------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|\n| X_ | [ndarray](`ndarray`) | All evaluated points, shape (n_samples, n_features). |\n| y_ | [ndarray](`ndarray`) | Function values at X_, shape (n_samples,). For multi-objective problems, these are the converted single-objective values. |\n| y_mo | [ndarray](`ndarray`) or None | Multi-objective function values, shape (n_samples, n_objectives). None for single-objective problems. |\n| best_x_ | [ndarray](`ndarray`) | Best point found, shape (n_features,). |\n| best_y_ | [float](`float`) | Best function value found. |\n| n_iter_ | [int](`int`) | Number of iterations performed. This is not the same as counter. Provided for compatibility with scipy.optimize routines. |\n| counter | [int](`int`) | Total number of function evaluations. |\n| success_rate | [float](`float`) | Rolling success rate over the last window_size evaluations. A success is counted when a new evaluation improves upon the best value found so far. |\n| warnings_filter | [str](`str`) | Filter for warnings during optimization. |\n| max_surrogate_points | [int](`int`) or None | Maximum number of points for surrogate fitting. |\n| selection_method | [str](`str`) | Point selection method. |\n| acquisition_failure_strategy | [str](`str`) | Strategy for handling acquisition failures ('random'). |\n| mean_X | [ndarray](`ndarray`) or None | Aggregated unique design points (if repeats_surrogate > 1). |\n| mean_y | [ndarray](`ndarray`) or None | Mean y values per design point (if repeats_surrogate > 1). |\n| var_y | [ndarray](`ndarray`) or None | Variance of y values per design point (if repeats_surrogate > 1). |\n| min_mean_X | [ndarray](`ndarray`) or None | X value of best mean y (if repeats_surrogate > 1). |\n| min_mean_y | [float](`float`) or None | Best mean y value (if repeats_surrogate > 1). |\n| min_var_y | [float](`float`) or None | Variance of best mean y (if repeats_surrogate > 1). |\n| de_x0_prob | [float](`float`) | Probability of using the best point as starting point for differential evolution. |\n| tricands_fringe | [bool](`bool`) | Whether to use the fringe of the design space for the initial design. |\n| prob_de_tricands | [float](`float`) | Probability of using differential evolution as an optimizer on the surrogate model. |\n\n#### Examples {.doc-section .doc-section-examples}\n\n\n::: {#3bd05c7d .cell execution_count=1}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 1: Basic usage (deterministic function)\nbounds = [(-5, 5), (-5, 5)]\noptimizer = SpotOptim(fun=objective, bounds=bounds, max_iter=10, n_initial=5, verbose=True)\nresult = optimizer.optimize()\nprint(\"Best x:\", result.x)\nprint(\"Best f(x):\", result.fun)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 1.852807\nIter 1 | Best: 0.660849 | Rate: 1.00 | Evals: 60.0%\nIter 2 | Best: 0.660849 | Curr: 0.736318 | Rate: 0.50 | Evals: 70.0%\nIter 3 | Best: 0.178773 | Rate: 0.67 | Evals: 80.0%\nIter 4 | Best: 0.081480 | Rate: 0.75 | Evals: 90.0%\nIter 5 | Best: 0.059414 | Rate: 0.80 | Evals: 100.0%\nBest x: [-0.13946178 0.19991054]\nBest f(x): 0.0594138116481199\n```\n:::\n:::\n\n\n::: {#e9954d18 .cell execution_count=2}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 2: With custom variable names\noptimizer = SpotOptim(\n fun=objective,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"param1\", \"param2\"],\n max_iter=10,\n n_initial=5\n)\nresult = optimizer.optimize()\n# Ensure we can use custom names in plots\noptimizer.plot_surrogate(show=False)\n```\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim_files/figure-html/cell-3-output-1.png){width=1125 height=950}\n:::\n:::\n\n\n::: {#006c6cca .cell execution_count=3}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\n# Example 3: Noisy function with repeated evaluations\ndef noisy_objective(X):\n base = np.sum(X**2, axis=1)\n noise = np.random.normal(0, 0.1, size=base.shape)\n return base + noise\n\noptimizer = SpotOptim(\n fun=noisy_objective,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=30,\n n_initial=10,\n repeats_initial=3, # Evaluate each initial point 3 times\n repeats_surrogate=2, # Evaluate each new point 2 times\n seed=42, # For reproducibility\n verbose=True\n)\nresult = optimizer.optimize()\n\n# Access noise statistics\nprint(\"Unique design points:\", optimizer.mean_X.shape[0])\nprint(\"Best mean value:\", optimizer.min_mean_y)\nprint(\"Variance at best point:\", optimizer.min_var_y)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 3.784490, mean best: f(x) = 3.897231\nUnique design points: 10\nBest mean value: 3.897231316535167\nVariance at best point: 0.01574581237168188\n```\n:::\n:::\n\n\n::: {#57f7baa6 .cell execution_count=4}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef noisy_objective(X):\n base = np.sum(X**2, axis=1)\n noise = np.random.normal(0, 0.1, size=base.shape)\n return base + noise\n\n# Example 4: Noisy function with OCBA (Optimal Computing Budget Allocation)\noptimizer_ocba = SpotOptim(\n fun=noisy_objective,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=50,\n n_initial=10,\n repeats_initial=2, # Initial repeats\n repeats_surrogate=1, # Surrogate repeats\n ocba_delta=3, # Allocate 3 additional evaluations per iteration\n seed=42,\n verbose=True\n)\nresult = optimizer_ocba.optimize()\n\n# OCBA intelligently re-evaluates promising points to reduce uncertainty\nprint(\"Total evaluations:\", result.nfev)\nprint(\"Unique design points:\", optimizer_ocba.mean_X.shape[0])\nprint(\"Best mean value:\", optimizer_ocba.min_mean_y)\nprint(\"Variance at best point:\", optimizer_ocba.min_var_y)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 3.734393, mean best: f(x) = 3.842155\n\nIn get_ocba():\nmeans: [49.11419203 26.21685016 7.98802093 6.31788609 6.16395351 18.69399049\n 3.84215472 5.54382744 9.93117406 36.60267168]\nvars: [4.40284305e-03 6.35744853e-04 1.33640624e-08 3.37927306e-03\n 1.64745915e-03 1.91555606e-03 1.16126758e-02 1.00799409e-03\n 6.73858270e-13 2.56053422e-03]\ndelta: 3\nn_designs: 10\nRatios: [6.17116575e-03 3.64806588e-03 2.23358257e-06 1.58383644e+00\n 8.77929372e-01 2.49475944e-02 5.05750153e+00 1.00000000e+00\n 5.22117127e-11 6.85366978e-03]\nBest: 6, Second best: 7\n OCBA: Adding 3 re-evaluation(s)\nIter 1 | Best: -0.126522 | Rate: 0.25 | Evals: 48.0% | Mean Best: -0.126522\nIter 2 | Best: -0.126522 | Curr: -0.053650 | Rate: 0.20 | Evals: 50.0% | Mean Curr: -0.053650\nIter 3 | Best: -0.126522 | Curr: 0.186753 | Rate: 0.17 | Evals: 52.0% | Mean Curr: 0.186753\nIter 4 | Best: -0.126522 | Curr: 0.090147 | Rate: 0.14 | Evals: 54.0% | Mean Curr: 0.090147\nIter 5 | Best: -0.126522 | Curr: 0.063073 | Rate: 0.12 | Evals: 56.0% | Mean Curr: 0.063073\nIter 6 | Best: -0.126522 | Curr: -0.005544 | Rate: 0.11 | Evals: 58.0% | Mean Curr: -0.005544\nIter 7 | Best: -0.126522 | Curr: 0.859237 | Rate: 0.10 | Evals: 60.0% | Mean Curr: 0.859237\nIter 8 | Best: -0.126522 | Curr: 0.568121 | Rate: 0.09 | Evals: 62.0% | Mean Curr: 0.568121\nIter 9 | Best: -0.126522 | Curr: 0.599008 | Rate: 0.08 | Evals: 64.0% | Mean Curr: 0.599008\nIter 10 | Best: -0.126522 | Curr: 0.012173 | Rate: 0.08 | Evals: 66.0% | Mean Curr: 0.012173\nIter 11 | Best: -0.126522 | Curr: 0.146368 | Rate: 0.07 | Evals: 68.0% | Mean Curr: 0.146368\nIter 12 | Best: -0.126522 | Curr: 0.188287 | Rate: 0.07 | Evals: 70.0% | Mean Curr: 0.188287\nIter 13 | Best: -0.126522 | Curr: 0.254861 | Rate: 0.06 | Evals: 72.0% | Mean Curr: 0.254861\nIter 14 | Best: -0.126522 | Curr: 0.026606 | Rate: 0.06 | Evals: 74.0% | Mean Curr: 0.026606\nIter 15 | Best: -0.126522 | Curr: 0.358788 | Rate: 0.06 | Evals: 76.0% | Mean Curr: 0.358788\nIter 16 | Best: -0.126522 | Curr: -0.122865 | Rate: 0.05 | Evals: 78.0% | Mean Curr: -0.122865\nIter 17 | Best: -0.126522 | Curr: 0.074514 | Rate: 0.05 | Evals: 80.0% | Mean Curr: 0.074514\nIter 18 | Best: -0.126522 | Curr: 0.092824 | Rate: 0.05 | Evals: 82.0% | Mean Curr: 0.092824\nIter 19 | Best: -0.126522 | Curr: 0.103061 | Rate: 0.05 | Evals: 84.0% | Mean Curr: 0.103061\nIter 20 | Best: -0.126522 | Curr: 0.016097 | Rate: 0.04 | Evals: 86.0% | Mean Curr: 0.016097\nIter 21 | Best: -0.126522 | Curr: 0.073018 | Rate: 0.04 | Evals: 88.0% | Mean Curr: 0.073018\nIter 22 | Best: -0.126522 | Curr: 0.115369 | Rate: 0.04 | Evals: 90.0% | Mean Curr: 0.115369\nIter 23 | Best: -0.126522 | Curr: 0.054835 | Rate: 0.04 | Evals: 92.0% | Mean Curr: 0.054835\nIter 24 | Best: -0.126522 | Curr: -0.032242 | Rate: 0.04 | Evals: 94.0% | Mean Curr: -0.032242\nIter 25 | Best: -0.126522 | Curr: 0.129055 | Rate: 0.04 | Evals: 96.0% | Mean Curr: 0.129055\nIter 26 | Best: -0.126522 | Curr: 0.093620 | Rate: 0.03 | Evals: 98.0% | Mean Curr: 0.093620\nIter 27 | Best: -0.157692 | Rate: 0.07 | Evals: 100.0% | Mean Best: -0.157692\nTotal evaluations: 50\nUnique design points: 37\nBest mean value: -0.1576921303726005\nVariance at best point: 0.0\n```\n:::\n:::\n\n\n::: {#8bece16c .cell execution_count=5}\n``` {.python .cell-code}\nimport numpy as np\nimport shutil\nimport os\nfrom spotoptim import SpotOptim\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 5: With TensorBoard logging\ntb_dir = \"runs/my_optimization\"\noptimizer_tb = SpotOptim(\n fun=objective,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=15,\n n_initial=5,\n tensorboard_log=True, # Enable TensorBoard\n tensorboard_path=tb_dir, # Optional custom path\n verbose=True\n)\nresult = optimizer_tb.optimize()\n\n# View logs in browser: tensorboard --logdir=runs/my_optimization\nprint(\"Logs saved to:\", optimizer_tb.tensorboard_path)\n\n# Cleanup log dir\nif os.path.exists(tb_dir):\n shutil.rmtree(tb_dir)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging enabled: runs/my_optimization\nInitial best: f(x) = 4.586678\nIter 1 | Best: 4.586678 | Curr: 26.504009 | Rate: 0.00 | Evals: 40.0%\nIter 2 | Best: 4.540395 | Rate: 0.50 | Evals: 46.7%\nIter 3 | Best: 2.914050 | Rate: 0.67 | Evals: 53.3%\nIter 4 | Best: 2.734693 | Rate: 0.75 | Evals: 60.0%\nIter 5 | Best: 1.043600 | Rate: 0.80 | Evals: 66.7%\nIter 6 | Best: 0.028106 | Rate: 0.83 | Evals: 73.3%\nIter 7 | Best: 0.005212 | Rate: 0.86 | Evals: 80.0%\nIter 8 | Best: 0.000016 | Rate: 0.88 | Evals: 86.7%\nIter 9 | Best: 0.000001 | Rate: 0.89 | Evals: 93.3%\nIter 10 | Best: 0.000001 | Curr: 0.000001 | Rate: 0.80 | Evals: 100.0%\nTensorBoard writer closed. View logs with: tensorboard --logdir=runs/my_optimization\nLogs saved to: runs/my_optimization\n```\n:::\n:::\n\n\n::: {#0a2ab172 .cell execution_count=6}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.surrogate import Kriging\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 6: Using SpotOptim's Kriging surrogate\nkriging_model = Kriging(\n noise=1e-10, # Regularization parameter\n kernel='gauss', # Gaussian/RBF kernel\n min_theta=-3.0, # Min log10(theta) bound\n max_theta=2.0, # Max log10(theta) bound\n seed=42\n)\noptimizer_kriging = SpotOptim(\n fun=objective,\n bounds=[(-5, 5), (-5, 5)],\n surrogate=kriging_model,\n max_iter=15,\n n_initial=5,\n seed=42,\n verbose=True\n)\nresult = optimizer_kriging.optimize()\nprint(\"Best solution found:\", result.x)\nprint(\"Best value:\", result.fun)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 3.251349\nIter 1 | Best: 3.251349 | Curr: 4.425619 | Rate: 0.00 | Evals: 40.0%\nIter 2 | Best: 1.617693 | Rate: 0.50 | Evals: 46.7%\nIter 3 | Best: 1.617693 | Curr: 18.716279 | Rate: 0.33 | Evals: 53.3%\nIter 4 | Best: 0.839564 | Rate: 0.50 | Evals: 60.0%\nIter 5 | Best: 0.102879 | Rate: 0.60 | Evals: 66.7%\nIter 6 | Best: 0.000147 | Rate: 0.67 | Evals: 73.3%\nIter 7 | Best: 0.000024 | Rate: 0.71 | Evals: 80.0%\nIter 8 | Best: 0.000024 | Curr: 0.000089 | Rate: 0.62 | Evals: 86.7%\nIter 9 | Best: 0.000024 | Curr: 0.000179 | Rate: 0.56 | Evals: 93.3%\nIter 10 | Best: 0.000024 | Curr: 0.000210 | Rate: 0.50 | Evals: 100.0%\nBest solution found: [0.00216696 0.00435049]\nBest value: 2.3622487424062804e-05\n```\n:::\n:::\n\n\n::: {#ded73223 .cell execution_count=7}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom sklearn.gaussian_process import GaussianProcessRegressor\nfrom sklearn.gaussian_process.kernels import RBF, ConstantKernel, WhiteKernel\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 7: Using sklearn Gaussian Process with custom kernel\n# Custom kernel: constant * RBF + white noise\ncustom_kernel = ConstantKernel(1.0, (1e-2, 1e2)) * RBF(\n length_scale=1.0, length_scale_bounds=(1e-1, 10.0)\n) + WhiteKernel(noise_level=1e-5, noise_level_bounds=(1e-10, 1e-1))\n\ngp_custom = GaussianProcessRegressor(\n kernel=custom_kernel,\n n_restarts_optimizer=15,\n normalize_y=True,\n random_state=42\n)\n\noptimizer_custom_gp = SpotOptim(\n fun=objective,\n bounds=[(-5, 5), (-5, 5)],\n surrogate=gp_custom,\n max_iter=15,\n n_initial=5,\n seed=42\n)\nresult = optimizer_custom_gp.optimize()\n```\n:::\n\n\n::: {#8880a111 .cell execution_count=8}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom sklearn.ensemble import RandomForestRegressor\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 8: Using Random Forest as surrogate\nrf_model = RandomForestRegressor(\n n_estimators=100,\n max_depth=10,\n random_state=42\n)\n\noptimizer_rf = SpotOptim(\n fun=objective,\n bounds=[(-5, 5), (-5, 5)],\n surrogate=rf_model,\n max_iter=15,\n n_initial=5,\n seed=42\n)\nresult = optimizer_rf.optimize()\n\n# Note: Random Forests don't provide uncertainty estimates,\n# so Expected Improvement (EI) may be less effective.\n# Consider using acquisition='y' for pure exploitation.\n```\n:::\n\n\n::: {#9944d989 .cell execution_count=9}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom sklearn.gaussian_process import GaussianProcessRegressor\nfrom sklearn.gaussian_process.kernels import Matern, RationalQuadratic, ConstantKernel, RBF\n\ndef objective(X):\n return np.sum(X**2, axis=1)\n\n# Example 9: Comparing different kernels for Gaussian Process\n# Matern kernel with nu=1.5 (once differentiable)\nkernel_matern15 = ConstantKernel(1.0) * Matern(length_scale=1.0, nu=1.5)\ngp_matern15 = GaussianProcessRegressor(kernel=kernel_matern15, normalize_y=True)\n\n# Matern kernel with nu=2.5 (twice differentiable, DEFAULT)\nkernel_matern25 = ConstantKernel(1.0) * Matern(length_scale=1.0, nu=2.5)\ngp_matern25 = GaussianProcessRegressor(kernel=kernel_matern25, normalize_y=True)\n\n# RBF kernel (infinitely differentiable, smooth)\nkernel_rbf = ConstantKernel(1.0) * RBF(length_scale=1.0)\ngp_rbf = GaussianProcessRegressor(kernel=kernel_rbf, normalize_y=True)\n\n# Rational Quadratic kernel (mixture of RBF kernels)\nkernel_rq = ConstantKernel(1.0) * RationalQuadratic(length_scale=1.0, alpha=1.0)\ngp_rq = GaussianProcessRegressor(kernel=kernel_rq, normalize_y=True)\n\n# Use any of these as surrogate\noptimizer_rbf = SpotOptim(fun=objective, bounds=[(-5, 5), (-5, 5)],\n surrogate=gp_rbf, max_iter=15, n_initial=5)\nresult = optimizer_rbf.optimize()\n```\n:::\n\n\n#### Methods\n\n| Name | Description |\n| --- | --- |\n| [aggregate_mean_var](#spotoptim.SpotOptim.SpotOptim.aggregate_mean_var) | Aggregate X and y values to compute mean and variance per group. |\n| [apply_ocba](#spotoptim.SpotOptim.SpotOptim.apply_ocba) | Apply Optimal Computing Budget Allocation for noisy functions. |\n| [apply_penalty_NA](#spotoptim.SpotOptim.SpotOptim.apply_penalty_NA) | Replace NaN and infinite values with penalty plus random noise. |\n| [check_size_initial_design](#spotoptim.SpotOptim.SpotOptim.check_size_initial_design) | Validate that initial design has sufficient points for surrogate fitting. |\n| [curate_initial_design](#spotoptim.SpotOptim.SpotOptim.curate_initial_design) | Remove duplicates and ensure sufficient unique points in initial design. |\n| [detect_var_type](#spotoptim.SpotOptim.SpotOptim.detect_var_type) | Auto-detect variable types based on factor mappings. |\n| [determine_termination](#spotoptim.SpotOptim.SpotOptim.determine_termination) | Determine termination reason for optimization. |\n| [evaluate_function](#spotoptim.SpotOptim.SpotOptim.evaluate_function) | Evaluate objective function at points X. |\n| [gen_design_table](#spotoptim.SpotOptim.SpotOptim.gen_design_table) | Generate a table of the design or results. |\n| [generate_initial_design](#spotoptim.SpotOptim.SpotOptim.generate_initial_design) | Generate initial space-filling design using Latin Hypercube Sampling. |\n| [get_best_hyperparameters](#spotoptim.SpotOptim.SpotOptim.get_best_hyperparameters) | Get the best hyperparameter configuration found during optimization. |\n| [get_best_xy_initial_design](#spotoptim.SpotOptim.SpotOptim.get_best_xy_initial_design) | Determine and store the best point from initial design. |\n| [get_design_table](#spotoptim.SpotOptim.SpotOptim.get_design_table) | Get a table string showing the search space design before optimization. |\n| [get_experiment_filename](#spotoptim.SpotOptim.SpotOptim.get_experiment_filename) | Generate experiment filename (suffix '_exp.pkl') from prefix. |\n| [get_importance](#spotoptim.SpotOptim.SpotOptim.get_importance) | Calculate variable importance scores. |\n| [get_initial_design](#spotoptim.SpotOptim.SpotOptim.get_initial_design) | Generate or process initial design points. |\n| [get_ocba](#spotoptim.SpotOptim.SpotOptim.get_ocba) | Optimal Computing Budget Allocation (OCBA). |\n| [get_ocba_X](#spotoptim.SpotOptim.SpotOptim.get_ocba_X) | Calculate OCBA allocation (by calling `get_ocba()`) and repeat input array X. |\n| [get_pickle_safe_optimizer](#spotoptim.SpotOptim.SpotOptim.get_pickle_safe_optimizer) | Create a pickle-safe copy of the optimizer. |\n| [get_ranks](#spotoptim.SpotOptim.SpotOptim.get_ranks) | Returns ranks of numbers within input array x. |\n| [get_result_filename](#spotoptim.SpotOptim.SpotOptim.get_result_filename) | Generate result filename (suffix '_res.pkl')from prefix. |\n| [get_results_table](#spotoptim.SpotOptim.SpotOptim.get_results_table) | Get a comprehensive table string of optimization results. |\n| [get_shape](#spotoptim.SpotOptim.SpotOptim.get_shape) | Get the shape of the objective function output. |\n| [get_stars](#spotoptim.SpotOptim.SpotOptim.get_stars) | Converts a list of values to a list of stars. |\n| [get_success_rate](#spotoptim.SpotOptim.SpotOptim.get_success_rate) | Get the current success rate of the optimization process. |\n| [handle_default_var_trans](#spotoptim.SpotOptim.SpotOptim.handle_default_var_trans) | Handle default variable transformations. |\n| [init_storage](#spotoptim.SpotOptim.SpotOptim.init_storage) | Initialize storage for optimization. |\n| [inverse_transform_X](#spotoptim.SpotOptim.SpotOptim.inverse_transform_X) | Transform parameter array from internal to original scale. |\n| [inverse_transform_value](#spotoptim.SpotOptim.SpotOptim.inverse_transform_value) | Apply inverse transformation to a single float value. |\n| [load_experiment](#spotoptim.SpotOptim.SpotOptim.load_experiment) | Load an experiment configuration from a pickle file ('*_exp.pkl'). |\n| [load_result](#spotoptim.SpotOptim.SpotOptim.load_result) | Load complete optimization results from a pickle file (suffix '_res.pkl') |\n| [map_to_factor_values](#spotoptim.SpotOptim.SpotOptim.map_to_factor_values) | Map internal integer factor values back to string labels. |\n| [mo2so](#spotoptim.SpotOptim.SpotOptim.mo2so) | Convert multi-objective values to single-objective. |\n| [modify_bounds_based_on_var_type](#spotoptim.SpotOptim.SpotOptim.modify_bounds_based_on_var_type) | Modify bounds based on variable types. |\n| [optimize](#spotoptim.SpotOptim.SpotOptim.optimize) | Run the optimization process. |\n| [optimize_acquisition_func](#spotoptim.SpotOptim.SpotOptim.optimize_acquisition_func) | Optimize the acquisition function to find the next point to evaluate. |\n| [plot_importance](#spotoptim.SpotOptim.SpotOptim.plot_importance) | Plot variable importance. |\n| [plot_important_hyperparameter_contour](#spotoptim.SpotOptim.SpotOptim.plot_important_hyperparameter_contour) | Plot surrogate contours using spotoptim.plot.visualization.plot_important_hyperparameter_contour. |\n| [plot_parameter_scatter](#spotoptim.SpotOptim.SpotOptim.plot_parameter_scatter) | Plot parameter distributions showing relationship between each parameter and objective. |\n| [plot_progress](#spotoptim.SpotOptim.SpotOptim.plot_progress) | Plot optimization progress using spotoptim.plot.visualization.plot_progress. |\n| [plot_surrogate](#spotoptim.SpotOptim.SpotOptim.plot_surrogate) | Plot the surrogate model for two dimensions. |\n| [print_best](#spotoptim.SpotOptim.SpotOptim.print_best) | Print the best solution found during optimization. |\n| [print_results](#spotoptim.SpotOptim.SpotOptim.print_results) | Alias for print(get_results_table()) for compatibility. |\n| [process_factor_bounds](#spotoptim.SpotOptim.SpotOptim.process_factor_bounds) | Process `bounds` to handle factor variables. |\n| [reinitialize_components](#spotoptim.SpotOptim.SpotOptim.reinitialize_components) | Reinitialize components that were excluded during pickling. |\n| [remove_nan](#spotoptim.SpotOptim.SpotOptim.remove_nan) | Remove rows where y contains NaN or inf values. |\n| [repair_non_numeric](#spotoptim.SpotOptim.SpotOptim.repair_non_numeric) | Round non-numeric values to integers based on variable type. |\n| [rm_initial_design_NA_values](#spotoptim.SpotOptim.SpotOptim.rm_initial_design_NA_values) | Remove NaN/inf values from initial design evaluations. |\n| [save_experiment](#spotoptim.SpotOptim.SpotOptim.save_experiment) | Save the experiment configuration to a pickle file (suffix '_exp.pkl') . |\n| [save_result](#spotoptim.SpotOptim.SpotOptim.save_result) | Save the complete optimization results to a pickle file. |\n| [fit_select_best_cluster](#spotoptim.SpotOptim.SpotOptim.fit_select_best_cluster) | Selects all points from the cluster with the smallest mean y value. |\n| [fit_select_distant_points](#spotoptim.SpotOptim.SpotOptim.fit_select_distant_points) | Selects k points that are distant from each other using K-means clustering. |\n| [select_new](#spotoptim.SpotOptim.SpotOptim.select_new) | Select rows from A that are not in X. |\n| [sensitivity_spearman](#spotoptim.SpotOptim.SpotOptim.sensitivity_spearman) | Compute and print Spearman correlation between parameters and objective values. |\n| [set_seed](#spotoptim.SpotOptim.SpotOptim.set_seed) | Set global random seeds for reproducibility. |\n| [setup_dimension_reduction](#spotoptim.SpotOptim.SpotOptim.setup_dimension_reduction) | Set up dimension reduction by identifying fixed dimensions. |\n| [store_mo](#spotoptim.SpotOptim.SpotOptim.store_mo) | Store multi-objective values in self.y_mo. |\n| [suggest_next_infill_point](#spotoptim.SpotOptim.SpotOptim.suggest_next_infill_point) | Suggest next point to evaluate (dispatcher). |\n| [to_all_dim](#spotoptim.SpotOptim.SpotOptim.to_all_dim) | Expand reduced-dimensional points to full-dimensional representation. |\n| [to_red_dim](#spotoptim.SpotOptim.SpotOptim.to_red_dim) | Reduce full-dimensional points to optimization space. |\n| [transform_X](#spotoptim.SpotOptim.SpotOptim.transform_X) | Transform parameter array from original to internal scale. |\n| [transform_bounds](#spotoptim.SpotOptim.SpotOptim.transform_bounds) | Transform bounds from original to internal scale. |\n| [transform_value](#spotoptim.SpotOptim.SpotOptim.transform_value) | Apply transformation to a single float value. |\n| [update_repeats_infill_points](#spotoptim.SpotOptim.SpotOptim.update_repeats_infill_points) | Repeat infill point for noisy function evaluation. |\n| [update_stats](#spotoptim.SpotOptim.SpotOptim.update_stats) | Update optimization statistics. |\n| [update_storage](#spotoptim.SpotOptim.SpotOptim.update_storage) | Update storage (`X_`, `y_`) with new evaluation points. |\n| [update_success_rate](#spotoptim.SpotOptim.SpotOptim.update_success_rate) | Update the rolling success rate of the optimization process. |\n| [validate_x0](#spotoptim.SpotOptim.SpotOptim.validate_x0) | Validate and process starting point x0. Called in `__init__` and `optimize`. |\n\n##### aggregate_mean_var { #spotoptim.SpotOptim.SpotOptim.aggregate_mean_var }\n\n```python\nSpotOptim.SpotOptim.aggregate_mean_var(X, y)\n```\n\nAggregate X and y values to compute mean and variance per group.\n\nFor repeated evaluations at the same design point, this method computes\nthe mean function value and variance (using population variance, ddof=0).\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Design points, shape (n_samples, n_features). | _required_ |\n| y | [ndarray](`ndarray`) | Function values, shape (n_samples,). | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| tuple | [Tuple](`typing.Tuple`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`)\\] | A tuple containing: * X_agg (ndarray): Unique design points, shape (n_groups, n_features) * y_mean (ndarray): Mean y values per group, shape (n_groups,) * y_var (ndarray): Variance of y values per group, shape (n_groups,) |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#3f102b8e .cell execution_count=10}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n repeats_initial=2)\nX = np.array([[1, 2], [3, 4], [1, 2]])\ny = np.array([1, 2, 3])\nX_agg, y_mean, y_var = opt.aggregate_mean_var(X, y)\nprint(X_agg.shape)\nprint(y_mean)\nprint(y_var)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(2, 2)\n[2. 2.]\n[1. 0.]\n```\n:::\n:::\n\n\n##### apply_ocba { #spotoptim.SpotOptim.SpotOptim.apply_ocba }\n\n```python\nSpotOptim.SpotOptim.apply_ocba()\n```\n\nApply Optimal Computing Budget Allocation for noisy functions.\n\nDetermines which existing design points should be re-evaluated based on\nOCBA algorithm. This method computes optimal budget allocation to improve\nthe quality of the estimated best design.\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|---------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|\n| | [Optional](`typing.Optional`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`)\\] | Optional[ndarray]: Array of design points to re-evaluate, shape (n_re_eval, n_features). Returns None if OCBA conditions are not met or OCBA is disabled. |\n\n###### Note {.doc-section .doc-section-note}\n\nOCBA is only applied when:\n * (self.repeats_initial > 1) or (self.repeats_surrogate > 1)\n * self.ocba_delta > 0\n * All variances are > 0\n * At least 3 design points exist\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#9871d209 .cell execution_count=11}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(\n fun=lambda X: np.sum(X**2, axis=1) + np.random.normal(0, 0.1, X.shape[0]),\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n repeats_surrogate=2,\n ocba_delta=5,\n verbose=True\n)\n# Simulate optimization state (normally done in optimize())\nopt.mean_X = np.array([[1, 2], [0, 0], [2, 1]])\nopt.mean_y = np.array([5.0, 0.1, 5.0])\nopt.var_y = np.array([0.1, 0.05, 0.15])\nX_ocba = opt.apply_ocba()\n# OCBA: Adding 5 re-evaluation(s).\n# The following should be true:\nprint(X_ocba.shape[0] == 5)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\n\nIn get_ocba():\nmeans: [5. 0.1 5. ]\nvars: [0.1 0.05 0.15]\ndelta: 5\nn_designs: 3\nRatios: [1. 1.11803399 1.5 ]\nBest: 1, Second best: 0\n OCBA: Adding 5 re-evaluation(s)\nTrue\n```\n:::\n:::\n\n\nOCBA skipped - insufficient points\n\n::: {#596653c7 .cell execution_count=12}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt2 = SpotOptim(\n fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n repeats_surrogate=2,\n ocba_delta=5,\n verbose=True\n)\nopt2.mean_X = np.array([[1, 2], [0, 0]])\nopt2.mean_y = np.array([5.0, 0.1])\nopt2.var_y = np.array([0.1, 0.05])\nX_ocba = opt2.apply_ocba()\n# Warning: OCBA skipped (need >2 points with variance > 0)\nprint(X_ocba is None)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nTrue\n```\n:::\n:::\n\n\n##### apply_penalty_NA { #spotoptim.SpotOptim.SpotOptim.apply_penalty_NA }\n\n```python\nSpotOptim.SpotOptim.apply_penalty_NA(\n y,\n y_history=None,\n penalty_value=None,\n sd=0.1,\n)\n```\n\nReplace NaN and infinite values with penalty plus random noise.\nUsed in the optimize() method after function evaluations.\n\nThis method follows the approach from spotpython.utils.repair.apply_penalty_NA,\nreplacing NaN/inf values with a penalty value plus random noise to avoid\nidentical penalty values.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|---------------|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|\n| y | [ndarray](`ndarray`) | Array of objective function values to be repaired. | _required_ |\n| y_history | [ndarray](`ndarray`) | Historical objective function values used for computing penalty statistics. If None, uses y itself. Default is None. | `None` |\n| penalty_value | [float](`float`) | Value to replace NaN/inf with. If None, computes penalty as: max(finite_y_history) + 3 * std(finite_y_history). If all values are NaN/inf or only one finite value exists, falls back to self.penalty_val. Default is None. | `None` |\n| sd | [float](`float`) | Standard deviation for normal distributed random noise added to penalty. Default is 0.1. | `0.1` |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Array with NaN/inf replaced by penalty_value + random noise (normal distributed with mean 0 and standard deviation sd). |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#c59fdb1e .cell execution_count=13}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1), bounds=[(-5, 5)])\ny_hist = np.array([1.0, 2.0, 3.0, 5.0])\ny_new = np.array([4.0, np.nan, np.inf])\ny_clean = opt.apply_penalty_NA(y_new, y_history=y_hist)\nprint(f\"np.all(np.isfinite(y_clean)): {np.all(np.isfinite(y_clean))}\")\nprint(f\"y_clean: {y_clean}\")\n# NaN/inf replaced with worst value from history + 3*std + noise\nprint(f\"y_clean[1] > 5.0: {y_clean[1] > 5.0}\") # Should be larger than max finite value in history\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nnp.all(np.isfinite(y_clean)): True\ny_clean: [ 4. 10.1774006 10.0389413]\ny_clean[1] > 5.0: True\n```\n:::\n:::\n\n\n##### check_size_initial_design { #spotoptim.SpotOptim.SpotOptim.check_size_initial_design }\n\n```python\nSpotOptim.SpotOptim.check_size_initial_design(y0, n_evaluated)\n```\n\nValidate that initial design has sufficient points for surrogate fitting.\n\nChecks if the number of valid initial design points meets the minimum\nrequirement for fitting a surrogate model. The minimum required is the\nsmaller of:\n * (a) typical minimum for surrogate fitting (3 for multi-dimensional, 2 for 1D), or\n * (b) what the user requested (`n_initial`).\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-------------|----------------------|-------------------------------------------------------------------------------|------------|\n| y0 | [ndarray](`ndarray`) | Function values at initial design points (after filtering), shape (n_valid,). | _required_ |\n| n_evaluated | [int](`int`) | Original number of points evaluated before filtering. | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n###### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|------------------------------------------------------------------|\n| | [ValueError](`ValueError`) | If the number of valid points is less than the minimum required. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#b1ad3d9e .cell execution_count=14}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=10\n)\n# Sufficient points - no error\ny0 = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\nopt.check_size_initial_design(y0, n_evaluated=10)\n\n# Insufficient points - raises ValueError\ny0_small = np.array([1.0])\ntry:\n opt.check_size_initial_design(y0_small, n_evaluated=10)\nexcept ValueError as e:\n print(f\"Error: {e}\")\n\n# With verbose output\nopt_verbose = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=10,\n verbose=True\n)\ny0_reduced = np.array([1.0, 2.0, 3.0]) # Less than n_initial but valid\nopt_verbose.check_size_initial_design(y0_reduced, n_evaluated=10)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nError: Insufficient valid initial design points: only 1 finite value(s) out of 10 evaluated. Need at least 3 points to fit surrogate model. Please check your objective function or increase n_initial.\nTensorBoard logging disabled\nNote: Initial design size (3) is smaller than requested (10) due to NaN/inf values\n```\n:::\n:::\n\n\n##### curate_initial_design { #spotoptim.SpotOptim.SpotOptim.curate_initial_design }\n\n```python\nSpotOptim.SpotOptim.curate_initial_design(X0)\n```\n\nRemove duplicates and ensure sufficient unique points in initial design.\n\nThis method handles deduplication that can occur after rounding integer/factor\nvariables. If duplicates are found, it generates additional points to reach\nthe target n_initial unique points. Also handles repeating points when\nrepeats_initial > 1.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-------------------------------------------------------------------------|------------|\n| X0 | [ndarray](`ndarray`) | Initial design points in internal scale, shape (n_samples, n_features). | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Curated initial design with duplicates removed and repeated if necessary, shape (n_unique * repeats_initial, n_features). |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#db4658b1 .cell execution_count=15}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=10,\n var_type=['int', 'int'] # Integer variables may cause duplicates\n)\nX0 = opt.get_initial_design()\nX0_curated = opt.curate_initial_design(X0)\nX0_curated.shape[0] == 10 # Should have n_initial unique points\n```\n\n::: {.cell-output .cell-output-display execution_count=15}\n```\nTrue\n```\n:::\n:::\n\n\n::: {#30c49c82 .cell execution_count=16}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n# With repeats\nopt_repeat = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n repeats_initial=3\n)\nX0 = opt_repeat.get_initial_design()\nX0_curated = opt_repeat.curate_initial_design(X0)\nX0_curated.shape[0] == 15 # 5 unique points * 3 repeats\n```\n\n::: {.cell-output .cell-output-display execution_count=16}\n```\nTrue\n```\n:::\n:::\n\n\n##### detect_var_type { #spotoptim.SpotOptim.SpotOptim.detect_var_type }\n\n```python\nSpotOptim.SpotOptim.detect_var_type()\n```\n\nAuto-detect variable types based on factor mappings.\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|----------------|-------------------------------------------------------------------------------------------------------------------------------------------|\n| list | [list](`list`) | List of variable types ('factor' or 'float') for each dimension. Dimensions with factor mappings are assigned 'factor', others 'float'. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#376ccd61 .cell execution_count=17}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\n\n# Define a simple objective mapping names to values for demonstration\ndef objective(X):\n # X has shape (n_samples, n_dimensions)\n return X[:, 0] + X[:, 1]\n\n# The first dimension has factor levels ('red', 'green', 'blue')\n# The second dimension is continuous bounds (0, 10)\nspot = SpotOptim(fun=objective, bounds=[('red', 'green', 'blue'), (0, 10)])\nprint(spot.detect_var_type())\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n['factor', 'float']\n```\n:::\n:::\n\n\n##### determine_termination { #spotoptim.SpotOptim.SpotOptim.determine_termination }\n\n```python\nSpotOptim.SpotOptim.determine_termination(timeout_start)\n```\n\nDetermine termination reason for optimization.\n\nChecks the termination conditions and returns an appropriate message\nindicating why the optimization stopped. Three possible termination\nconditions are checked in order of priority:\n 1. Maximum number of evaluations reached\n 2. Maximum time limit exceeded\n 3. Successful completion (neither limit reached)\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|---------------|------------------|------------------------------------------------|------------|\n| timeout_start | [float](`float`) | Start time of optimization (from time.time()). | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------------|--------------------------------------------|\n| str | [str](`str`) | Message describing the termination reason. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#8eec2cff .cell execution_count=18}\n``` {.python .cell-code}\nimport numpy as np\nimport time\nfrom spotoptim import SpotOptim\nopt = SpotOptim(\n fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_iter=20,\n max_time=10.0\n)\n# Case 1: Maximum evaluations reached\nopt.y_ = np.zeros(20) # Simulate 20 evaluations\nstart_time = time.time()\nmsg = opt.determine_termination(start_time)\nprint(msg)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nOptimization terminated: maximum evaluations (20) reached\n```\n:::\n:::\n\n\n::: {#7f2c94b5 .cell execution_count=19}\n``` {.python .cell-code}\n# Case 2: Time limit exceeded\nimport numpy as np\nimport time\nfrom spotoptim import SpotOptim\nopt.y_ = np.zeros(10) # Only 10 evaluations\nstart_time = time.time() - 700 # Simulate 11.67 minutes elapsed\nmsg = opt.determine_termination(start_time)\nprint(msg)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nOptimization terminated: time limit (10.00 min) reached\n```\n:::\n:::\n\n\n::: {#55fe46aa .cell execution_count=20}\n``` {.python .cell-code}\n# Case 3: Successful completion\nimport numpy as np\nimport time\nfrom spotoptim import SpotOptim\nopt.y_ = np.zeros(10) # Under max_iter\nstart_time = time.time() # Just started\nmsg = opt.determine_termination(start_time)\nprint(msg)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nOptimization finished successfully\n```\n:::\n:::\n\n\n##### evaluate_function { #spotoptim.SpotOptim.SpotOptim.evaluate_function }\n\n```python\nSpotOptim.SpotOptim.evaluate_function(X)\n```\n\nEvaluate objective function at points X.\nUsed in the optimize() method to evaluate the objective function.\n\nInput Space: `X` is expected in Transformed and Mapped Space (Internal scale, Reduced dimensions).\nProcess as follows:\n 1. Expands `X` to Transformed Space (Full dimensions) if dimension reduction is active.\n 2. Inverse transforms `X` to Natural Space (Original scale).\n 3. Evaluates the user function with points in Natural Space.\n\nIf dimension reduction is active, expands `X` to full dimensions before evaluation.\nSupports both single-objective and multi-objective functions. For multi-objective\nfunctions, converts to single-objective using `mo2so` method.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|--------------------------------------------------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Points to evaluate in Transformed and Mapped Space, shape (n_samples, n_reduced_features). | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|--------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Function values, shape (n_samples,). |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#6fc9a55e .cell execution_count=21}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n# Single-objective function\nopt_so = SpotOptim(\n fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5\n)\nX = np.array([[1.0, 2.0], [3.0, 4.0]])\ny = opt_so.evaluate_function(X)\nprint(f\"Single-objective output: {y}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSingle-objective output: [ 5. 25.]\n```\n:::\n:::\n\n\n::: {#054b96e8 .cell execution_count=22}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n# Multi-objective function (default: use first objective)\nopt_mo = SpotOptim(\n fun=lambda X: np.column_stack([\n np.sum(X**2, axis=1),\n np.sum((X-1)**2, axis=1)\n ]),\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5\n)\ny_mo = opt_mo.evaluate_function(X)\nprint(f\"Multi-objective output (first obj): {y_mo}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nMulti-objective output (first obj): [ 5. 25.]\n```\n:::\n:::\n\n\n##### gen_design_table { #spotoptim.SpotOptim.SpotOptim.gen_design_table }\n\n```python\nSpotOptim.SpotOptim.gen_design_table(precision=4, tablefmt='github')\n```\n\nGenerate a table of the design or results.\n\nIf optimization has been run (results available), returns the results table.\nOtherwise, returns the design table (search space configuration).\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------|--------------|-----------------------------------------------------------|------------|\n| tablefmt | [str](`str`) | Table format. Defaults to 'github'. | `'github'` |\n| precision | [int](`int`) | Number of decimal places for float values. Defaults to 4. | `4` |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------------|-------------------------|\n| str | [str](`str`) | Formatted table string. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#4f941a7e .cell execution_count=23}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-10, 10), (0, 1)],\n var_name=[\"x1\", \"x2\", \"x3\"],\n var_type=[\"float\", \"int\", \"float\"],\n max_iter=20,\n n_initial=10\n)\ntable = opt.gen_design_table()\nprint(table)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n| name | type | lower | upper | default | transform |\n|--------|--------|---------|---------|-----------|-------------|\n| x1 | float | -5 | 5 | 0 | - |\n| x2 | int | -10 | 10 | 0 | - |\n| x3 | float | 0 | 1 | 0.5 | - |\n```\n:::\n:::\n\n\n##### generate_initial_design { #spotoptim.SpotOptim.SpotOptim.generate_initial_design }\n\n```python\nSpotOptim.SpotOptim.generate_initial_design()\n```\n\nGenerate initial space-filling design using Latin Hypercube Sampling.\nUsed in the optimize() method to create the initial set of design points.\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|-------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Initial design points, shape (n_initial, n_features). |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#457cce28 .cell execution_count=24}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=10)\nX0 = opt.generate_initial_design()\nprint(X0.shape)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(10, 2)\n```\n:::\n:::\n\n\n##### get_best_hyperparameters { #spotoptim.SpotOptim.SpotOptim.get_best_hyperparameters }\n\n```python\nSpotOptim.SpotOptim.get_best_hyperparameters(as_dict=True)\n```\n\nGet the best hyperparameter configuration found during optimization.\n\nIf noise handling is active (repeats_initial > 1 or OCBA), this returns the parameter\nconfiguration associated with the best *mean* objective value. Otherwise, it returns\nthe configuration associated with the absolute best observed value.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|---------|----------------|---------------------------------------------------------------------------------------------------------------------------------|-----------|\n| as_dict | [bool](`bool`) | If True, returns a dictionary mapping parameter names to their values. If False, returns the raw numpy array. Defaults to True. | `True` |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|---------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|\n| | [Union](`typing.Union`)\\[[Dict](`typing.Dict`)\\[[str](`str`), [Any](`typing.Any`)\\], [np](`numpy`).[ndarray](`numpy.ndarray`), None\\] | Union[Dict[str, Any], np.ndarray, None]: The best hyperparameter configuration. Returns None if optimization hasn't started (no data). |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#3f49ec63 .cell execution_count=25}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere,\n bounds=[(-5, 5), (0, 10)],\n n_initial=5,\n var_name=[\"x\", \"y\"],\n verbose=True)\nopt.optimize()\nbest_params = opt.get_best_hyperparameters()\nprint(best_params['x']) # Should be close to 0\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 3.721601\nIter 1 | Best: 2.055418 | Rate: 1.00 | Evals: 30.0%\nIter 2 | Best: 1.730657 | Rate: 1.00 | Evals: 35.0%\nIter 3 | Best: 0.550680 | Rate: 1.00 | Evals: 40.0%\nIter 4 | Best: 0.000081 | Rate: 1.00 | Evals: 45.0%\nIter 5 | Best: 0.000081 | Curr: 0.000866 | Rate: 0.80 | Evals: 50.0%\nIter 6 | Best: 0.000000 | Rate: 0.83 | Evals: 55.0%\nIter 7 | Best: 0.000000 | Rate: 0.86 | Evals: 60.0%\nIter 8 | Best: 0.000000 | Rate: 0.88 | Evals: 65.0%\nIter 9 | Best: 0.000000 | Rate: 0.89 | Evals: 70.0%\nIter 10 | Best: 0.000000 | Rate: 0.90 | Evals: 75.0%\nIter 11 | Best: 0.000000 | Rate: 0.91 | Evals: 80.0%\nIter 12 | Best: 0.000000 | Rate: 0.92 | Evals: 85.0%\nIter 13 | Best: 0.000000 | Curr: 0.000000 | Rate: 0.85 | Evals: 90.0%\nIter 14 | Best: 0.000000 | Rate: 0.86 | Evals: 95.0%\nIter 15 | Best: 0.000000 | Curr: 0.000000 | Rate: 0.80 | Evals: 100.0%\n6.180801228753924e-06\n```\n:::\n:::\n\n\n##### get_best_xy_initial_design { #spotoptim.SpotOptim.SpotOptim.get_best_xy_initial_design }\n\n```python\nSpotOptim.SpotOptim.get_best_xy_initial_design()\n```\n\nDetermine and store the best point from initial design.\n\nFinds the best (minimum) function value in the initial design,\nstores the corresponding point and value in instance attributes,\nand optionally prints the results if verbose mode is enabled.\n\nFor noisy functions, also reports the mean best value.\n\n###### Note {.doc-section .doc-section-note}\n\nThis method assumes self.X_ and self.y_ have been initialized\nwith the initial design evaluations.\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#b2f4bc0a .cell execution_count=26}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n verbose=True\n)\n# Simulate initial design (normally done in optimize())\nopt.X_ = np.array([[1, 2], [0, 0], [2, 1]])\nopt.y_ = np.array([5.0, 0.0, 5.0])\nopt.get_best_xy_initial_design()\nprint(f\"Best x: {opt.best_x_}\")\nprint(f\"Best y: {opt.best_y_}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 0.000000\nBest x: [0 0]\nBest y: 0.0\n```\n:::\n:::\n\n\n::: {#74dc7698 .cell execution_count=27}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import noisy_sphere\n# With noisy function\nopt_noise = SpotOptim(\n fun=noisy_sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n repeats_surrogate=2,\n verbose=True\n)\nopt_noise.X_ = np.array([[1, 2], [0, 0], [2, 1]])\nopt_noise.y_ = np.array([5.0, 0.0, 5.0])\nopt_noise.min_mean_y = 0.5 # Simulated mean best\nopt_noise.get_best_xy_initial_design()\nprint(f\"Best x: {opt_noise.best_x_}\")\nprint(f\"Best y: {opt_noise.best_y_}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 0.000000, mean best: f(x) = 0.500000\nBest x: [0 0]\nBest y: 0.0\n```\n:::\n:::\n\n\n##### get_design_table { #spotoptim.SpotOptim.SpotOptim.get_design_table }\n\n```python\nSpotOptim.SpotOptim.get_design_table(tablefmt='github', precision=4)\n```\n\nGet a table string showing the search space design before optimization.\n\nThis method generates a table displaying the variable names, types, bounds,\nand defaults without requiring an optimization run. Useful for inspecting\nand documenting the search space configuration.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------|--------------|-----------------------------------------------------------|------------|\n| tablefmt | [str](`str`) | Table format for tabulate library. Defaults to 'github'. | `'github'` |\n| precision | [int](`int`) | Number of decimal places for float values. Defaults to 4. | `4` |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------------|-------------------------|\n| str | [str](`str`) | Formatted table string. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#2c4cbb53 .cell execution_count=28}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-10, 10), (0, 1)],\n var_name=[\"x1\", \"x2\", \"x3\"],\n var_type=[\"float\", \"int\", \"float\"],\n max_iter=20,\n n_initial=10\n)\ntable = opt.get_design_table()\nprint(table)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n| name | type | lower | upper | default | transform |\n|--------|--------|---------|---------|-----------|-------------|\n| x1 | float | -5 | 5 | 0 | - |\n| x2 | int | -10 | 10 | 0 | - |\n| x3 | float | 0 | 1 | 0.5 | - |\n```\n:::\n:::\n\n\n##### get_experiment_filename { #spotoptim.SpotOptim.SpotOptim.get_experiment_filename }\n\n```python\nSpotOptim.SpotOptim.get_experiment_filename(prefix)\n```\n\nGenerate experiment filename (suffix '_exp.pkl') from prefix.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|--------------|--------------------------|------------|\n| prefix | [str](`str`) | Prefix for the filename. | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------------|----------------------------------|\n| str | [str](`str`) | Filename with '_exp.pkl' suffix. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#95887fe2 .cell execution_count=29}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda x: x, bounds=[(0, 1)])\nexp_filename = opt.get_experiment_filename(prefix=\"my_experiment\")\nprint(exp_filename)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nmy_experiment_exp.pkl\n```\n:::\n:::\n\n\n##### get_importance { #spotoptim.SpotOptim.SpotOptim.get_importance }\n\n```python\nSpotOptim.SpotOptim.get_importance()\n```\n\nCalculate variable importance scores.\n\nImportance is computed as the normalized sensitivity of each parameter\nbased on the variation in objective values across the evaluated points.\nHigher scores indicate parameters that have more influence on the objective.\n\n###### The importance is calculated as {.doc-section .doc-section-the-importance-is-calculated-as}\n\n1. For each dimension, compute the correlation between parameter values\n and objective values\n2. Normalize to percentage scale (0-100)\n3. Higher values indicate more important parameters\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|-------------------------------------------|------------------------------------------------------------------|\n| | [List](`typing.List`)\\[[float](`float`)\\] | List[float]: Importance scores for each dimension (0-100 scale). |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#6074f104 .cell execution_count=30}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef test_func(X):\n # x0 has strong effect, x1 has weak effect\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=15,\n n_initial=5,\n seed=42\n)\nresult = opt.optimize()\nimportance = opt.get_importance()\nprint(f\"x0 importance: {importance[0]:.2f}\")\nprint(f\"x1 importance: {importance[1]:.2f}\")\n\n# Use table to display importance\ntable = opt.get_results_table(show_importance=True)\nprint(table)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nx0 importance: 72.58\nx1 importance: 27.42\n| name | type | default | lower | upper | tuned | transform | importance | stars |\n|--------|--------|-----------|---------|---------|---------|-------------|--------------|---------|\n| x0 | float | 0 | -5 | 5 | 0.0042 | - | 72.58 | * |\n| x1 | float | 0 | -5 | 5 | -0.0001 | - | 27.42 | . |\n\nInterpretation: ***: >99%, **: >75%, *: >50%, .: >10%\n```\n:::\n:::\n\n\n##### get_initial_design { #spotoptim.SpotOptim.SpotOptim.get_initial_design }\n\n```python\nSpotOptim.SpotOptim.get_initial_design(X0=None)\n```\n\nGenerate or process initial design points.\n\nHandles three scenarios:\n1. X0 is None: Generate space-filling design using LHS\n2. X0 is None but x0 is provided: Generate LHS and include x0 as first point\n3. X0 is provided: Transform and prepare user-provided initial design\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|-----------|\n| X0 | [ndarray](`ndarray`) | User-provided initial design points in original scale, shape (n_initial, n_features). If None, generates space-filling design. Defaults to None. | `None` |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|-----------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Initial design points in internal (transformed and reduced) scale, shape (n_initial, n_features_reduced). |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#a6804c62 .cell execution_count=31}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=10\n)\n# Generate default LHS design\nX0 = opt.get_initial_design()\nprint(X0.shape)\n# Provide custom initial design\nX0_custom = np.array([[0, 0], [1, 1], [2, 2]])\nX0_processed = opt.get_initial_design(X0_custom)\nprint(X0_processed.shape)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(10, 2)\n(3, 2)\n```\n:::\n:::\n\n\n##### get_ocba { #spotoptim.SpotOptim.SpotOptim.get_ocba }\n\n```python\nSpotOptim.SpotOptim.get_ocba(means, vars, delta, verbose=False)\n```\n\nOptimal Computing Budget Allocation (OCBA).\n\nCalculates budget recommendations for given means, variances, and incremental\nbudget using the OCBA algorithm.\n\n###### References {.doc-section .doc-section-references}\n\n[1] Chun-Hung Chen and Loo Hay Lee: Stochastic Simulation Optimization:\n An Optimal Computer Budget Allocation, pp. 49 and pp. 215\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|---------|----------------------|------------------------------------------------------|------------|\n| means | [ndarray](`ndarray`) | Array of means. | _required_ |\n| vars | [ndarray](`ndarray`) | Array of variances. | _required_ |\n| delta | [int](`int`) | Incremental budget. | _required_ |\n| verbose | [bool](`bool`) | If True, print debug information. Defaults to False. | `False` |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|-----------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Array of budget recommendations, or None if conditions not met. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#eddfd006 .cell execution_count=32}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1), bounds=[(-5, 5)])\nmeans = np.array([1, 2, 3, 4, 5])\nvars = np.array([1, 1, 9, 9, 4])\nallocations = opt.get_ocba(means, vars, 50)\nallocations\n```\n\n::: {.cell-output .cell-output-display execution_count=32}\n```\narray([11, 9, 19, 9, 2])\n```\n:::\n:::\n\n\n##### get_ocba_X { #spotoptim.SpotOptim.SpotOptim.get_ocba_X }\n\n```python\nSpotOptim.SpotOptim.get_ocba_X(X, means, vars, delta, verbose=False)\n```\n\nCalculate OCBA allocation (by calling `get_ocba()`) and repeat input array X.\nUsed in the `optimize()` method to generate new design points based on OCBA.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|---------|----------------------|------------------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Input array to be repeated, shape (n_designs, n_features). | _required_ |\n| means | [ndarray](`ndarray`) | Array of means for each design. | _required_ |\n| vars | [ndarray](`ndarray`) | Array of variances for each design. | _required_ |\n| delta | [int](`int`) | Incremental budget. | _required_ |\n| verbose | [bool](`bool`) | If True, print debug information. Defaults to False. | `False` |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|-----------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Repeated array of X based on OCBA allocation, or None if conditions not met. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#f4b65904 .cell execution_count=33}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1), bounds=[(-5, 5)])\nX = np.array([[1, 2], [4, 5], [7, 8]])\nmeans = np.array([1.5, 35, 550])\nvars = np.array([0.5, 50, 5000])\nX_new = opt.get_ocba_X(X, means, vars, delta=5, verbose=False)\nprint(X_new.shape[0]) # Should have 5 additional evaluations\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n5\n```\n:::\n:::\n\n\n##### get_pickle_safe_optimizer { #spotoptim.SpotOptim.SpotOptim.get_pickle_safe_optimizer }\n\n```python\nSpotOptim.SpotOptim.get_pickle_safe_optimizer(\n unpickleables='file_io',\n verbosity=0,\n)\n```\n\nCreate a pickle-safe copy of the optimizer.\n\nThis method creates a copy of the optimizer instance with unpickleable components removed\nor set to None to enable safe serialization.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|---------------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|\n| unpickleables | [str](`str`) | Type of unpickleable components to exclude. * \"file_io\": Excludes only file I/O components (tb_writer) and fun * \"all\": Excludes file I/O, fun, surrogate, and lhs_sampler Defaults to \"file_io\". | `'file_io'` |\n| verbosity | [int](`int`) | Verbosity level (0=silent, 1=basic, 2=detailed). Defaults to 0. | `0` |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|-----------|----------------------------------------------|---------------------------------------------------------------|\n| SpotOptim | [SpotOptim](`spotoptim.SpotOptim.SpotOptim`) | A copy of the optimizer with unpickleable components removed. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#85bb59c7 .cell execution_count=34}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(\n fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_iter=30,\n n_initial=10,\n seed=42\n)\n# Create pickle-safe copy excluding all unpickleables\nopt_safe = opt.get_pickle_safe_optimizer(unpickleables=\"all\", verbosity=1)\n```\n:::\n\n\n##### get_ranks { #spotoptim.SpotOptim.SpotOptim.get_ranks }\n\n```python\nSpotOptim.SpotOptim.get_ranks(x)\n```\n\nReturns ranks of numbers within input array x.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|---------------|------------|\n| x | [ndarray](`ndarray`) | Input array. | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|-------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Ranks array where ranks[i] is the rank of x[i]. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#8a726310 .cell execution_count=35}\n``` {.python .cell-code}\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1), bounds=[(-5, 5)])\nopt.get_ranks(np.array([2, 1]))\nopt.get_ranks(np.array([20, 10, 100]))\n```\n\n::: {.cell-output .cell-output-display execution_count=35}\n```\narray([1, 0, 2])\n```\n:::\n:::\n\n\n##### get_result_filename { #spotoptim.SpotOptim.SpotOptim.get_result_filename }\n\n```python\nSpotOptim.SpotOptim.get_result_filename(prefix)\n```\n\nGenerate result filename (suffix '_res.pkl')from prefix.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|--------------|--------------------------|------------|\n| prefix | [str](`str`) | Prefix for the filename. | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------------|----------------------------------|\n| str | [str](`str`) | Filename with '_res.pkl' suffix. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#6f6c9594 .cell execution_count=36}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda x: x, bounds=[(0, 1)])\nres_filename = opt.get_result_filename(prefix=\"my_experiment\")\nprint(res_filename)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nmy_experiment_res.pkl\n```\n:::\n:::\n\n\n##### get_results_table { #spotoptim.SpotOptim.SpotOptim.get_results_table }\n\n```python\nSpotOptim.SpotOptim.get_results_table(\n tablefmt='github',\n precision=4,\n show_importance=False,\n)\n```\n\nGet a comprehensive table string of optimization results.\n\nThis method generates a formatted table of the search space configuration,\nbest values found, and optionally variable importance scores.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------------|----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|\n| tablefmt | [str](`str`) | Table format for tabulate library. Options include: 'github', 'grid', 'simple', 'plain', 'html', 'latex', etc. Defaults to 'github'. | `'github'` |\n| precision | [int](`int`) | Number of decimal places for float values. Defaults to 4. | `4` |\n| show_importance | [bool](`bool`) | Whether to include importance scores. Importance is calculated as the normalized standard deviation of each parameter's effect on the objective. Requires multiple evaluations. Defaults to False. | `False` |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------------|------------------------------------------------------|\n| str | [str](`str`) | Formatted table string that can be printed or saved. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#666a9cd6 .cell execution_count=37}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\n# Example 1: Basic usage after optimization\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5), (-5, 5)],\n var_name=[\"x1\", \"x2\", \"x3\"],\n var_type=[\"float\", \"float\", \"float\"],\n max_iter=15,\n n_initial=10\n)\nresult = opt.optimize()\ntable = opt.get_results_table()\nprint(table)\ntable = opt.get_results_table(show_importance=True)\nprint(table)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n| name | type | default | lower | upper | tuned | transform |\n|--------|--------|-----------|---------|---------|---------|-------------|\n| x1 | float | 0 | -5 | 5 | -0.0284 | - |\n| x2 | float | 0 | -5 | 5 | -0.108 | - |\n| x3 | float | 0 | -5 | 5 | -0.0424 | - |\n| name | type | default | lower | upper | tuned | transform | importance | stars |\n|--------|--------|-----------|---------|---------|---------|-------------|--------------|---------|\n| x1 | float | 0 | -5 | 5 | -0.0284 | - | 50.31 | * |\n| x2 | float | 0 | -5 | 5 | -0.108 | - | 16.88 | . |\n| x3 | float | 0 | -5 | 5 | -0.0424 | - | 32.81 | . |\n\nInterpretation: ***: >99%, **: >75%, *: >50%, .: >10%\n```\n:::\n:::\n\n\n##### get_shape { #spotoptim.SpotOptim.SpotOptim.get_shape }\n\n```python\nSpotOptim.SpotOptim.get_shape(y)\n```\n\nGet the shape of the objective function output.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------------------------------------|------------|\n| y | [ndarray](`ndarray`) | Objective function output, shape (n_samples,) or (n_samples, n_objectives). | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|----------------------------------------------------------------------------------------|----------------------------------------------------------------------------|\n| tuple | [Tuple](`typing.Tuple`)\\[[int](`int`), [Optional](`typing.Optional`)\\[[int](`int`)\\]\\] | (n_samples, n_objectives) where n_objectives is None for single-objective. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#4eaeb293 .cell execution_count=38}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(\n fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5\n)\ny_single = np.array([1.0, 2.0, 3.0])\nn, m = opt.get_shape(y_single)\nprint(f\"n={n}, m={m}\")\ny_multi = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])\nn, m = opt.get_shape(y_multi)\nprint(f\"n={n}, m={m}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nn=3, m=None\nn=3, m=2\n```\n:::\n:::\n\n\n##### get_stars { #spotoptim.SpotOptim.SpotOptim.get_stars }\n\n```python\nSpotOptim.SpotOptim.get_stars(input_list)\n```\n\nConverts a list of values to a list of stars.\n\nUsed to visualize the importance of a variable.\nThresholds: >99: ***, >75: **, >50: *, >10: .\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|------------|----------------|--------------------------------------|------------|\n| input_list | [list](`list`) | A list of importance scores (0-100). | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|----------------|-------------------------|\n| list | [list](`list`) | A list of star strings. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#01b691ad .cell execution_count=39}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\n\ndef test_func(X):\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=15,\n n_initial=5,\n seed=42\n)\nopt.optimize()\nopt.get_stars([100, 75, 50, 10, 0])\n```\n\n::: {.cell-output .cell-output-display execution_count=39}\n```\n['***', '*', '.', '', '']\n```\n:::\n:::\n\n\n##### get_success_rate { #spotoptim.SpotOptim.SpotOptim.get_success_rate }\n\n```python\nSpotOptim.SpotOptim.get_success_rate()\n```\n\nGet the current success rate of the optimization process.\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|------------------|---------------------------|\n| float | [float](`float`) | The current success rate. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#7b3d64c0 .cell execution_count=40}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda x: x,\n bounds=[(-5, 5), (-5, 5)])\nprint(opt.get_success_rate())\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n0.0\n```\n:::\n:::\n\n\n##### handle_default_var_trans { #spotoptim.SpotOptim.SpotOptim.handle_default_var_trans }\n\n```python\nSpotOptim.SpotOptim.handle_default_var_trans()\n```\n\nHandle default variable transformations.\n\nSets var_trans to a list of None values if not specified, or normalizes\ntransformation names by converting 'id', 'None', or None to None.\nAlso validates that var_trans length matches the number of dimensions.\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n###### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|------------------------------------------|\n| | [ValueError](`ValueError`) | If var_trans length doesn't match n_dim. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#90dea518 .cell execution_count=41}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\n# Default behavior - all None\nspot = SpotOptim(fun=lambda x: x, bounds=[(0, 10), (0, 10)])\nprint(f\"spot.var_trans (should be [None, None]): {spot.var_trans}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nspot.var_trans (should be [None, None]): [None, None]\n```\n:::\n:::\n\n\n::: {#d604c23a .cell execution_count=42}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\n# Normalize transformation names\nspot = SpotOptim(fun=lambda x: x, bounds=[(1, 10), (1, 100)],\n var_trans=['log10', 'id'])\nprint(f\"spot.var_trans (should be ['log10', 'None']): {spot.var_trans}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nspot.var_trans (should be ['log10', 'None']): ['log10', None]\n```\n:::\n:::\n\n\n##### init_storage { #spotoptim.SpotOptim.SpotOptim.init_storage }\n\n```python\nSpotOptim.SpotOptim.init_storage(X0, y0)\n```\n\nInitialize storage for optimization.\n\nSets up the initial data structures needed for optimization tracking:\n- X_: Evaluated design points (in original scale)\n- y_: Function values at evaluated points\n- n_iter_: Iteration counter\n\nThen updates statistics by calling update_stats().\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-------------------------------------------------------------------------|------------|\n| X0 | [ndarray](`ndarray`) | Initial design points in internal scale, shape (n_samples, n_features). | _required_ |\n| y0 | [ndarray](`ndarray`) | Function values at X0, shape (n_samples,). | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#4806386d .cell execution_count=43}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5)\nX0 = np.array([[1, 2], [3, 4], [0, 1]])\ny0 = np.array([5.0, 25.0, 1.0])\nopt.init_storage(X0, y0)\nprint(f\"X_ = {opt.X_}\")\nprint(f\"y_ = {opt.y_}\")\nprint(f\"n_iter_ = {opt.n_iter_}\")\nprint(f\"counter = {opt.counter}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nX_ = [[1 2]\n [3 4]\n [0 1]]\ny_ = [ 5. 25. 1.]\nn_iter_ = 0\ncounter = 0\n```\n:::\n:::\n\n\n##### inverse_transform_X { #spotoptim.SpotOptim.SpotOptim.inverse_transform_X }\n\n```python\nSpotOptim.SpotOptim.inverse_transform_X(X)\n```\n\nTransform parameter array from internal to original scale.\n\nConverts from Transformed Space (Full Dimension) to Natural Space (Original).\nDoes NOT handle dimension expansion (un-mapping).\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Array in Transformed Space, shape (n_samples, n_features) | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Array in Natural Space |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#10469be3 .cell execution_count=44}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nimport numpy as np\nspot = SpotOptim(fun=sphere, bounds=[(1, 10)])\nX_trans = np.array([[0], [1], [2]])\nspot.inverse_transform_X(X_trans)\n```\n\n::: {.cell-output .cell-output-display execution_count=44}\n```\narray([[0],\n [1],\n [2]])\n```\n:::\n:::\n\n\n##### inverse_transform_value { #spotoptim.SpotOptim.SpotOptim.inverse_transform_value }\n\n```python\nSpotOptim.SpotOptim.inverse_transform_value(x, trans)\n```\n\nApply inverse transformation to a single float value.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|-----------------------------------------------|----------------------|------------|\n| x | [float](`float`) | Transformed value | _required_ |\n| trans | [Optional](`typing.Optional`)\\[[str](`str`)\\] | Transformation name. | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|------------------|----------------|\n| | [float](`float`) | Original value |\n\n###### Notes {.doc-section .doc-section-notes}\n\nSee also transform_value.\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#90abd289 .cell execution_count=45}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nspot = SpotOptim(fun=sphere, bounds=[(1, 10)])\nspot.inverse_transform_value(10, 'log10')\nspot.inverse_transform_value(100, 'log(x)')\n```\n\n::: {.cell-output .cell-output-display execution_count=45}\n```\nnp.float64(2.6881171418161356e+43)\n```\n:::\n:::\n\n\n##### load_experiment { #spotoptim.SpotOptim.SpotOptim.load_experiment }\n\n```python\nSpotOptim.SpotOptim.load_experiment(filename)\n```\n\nLoad an experiment configuration from a pickle file ('*_exp.pkl').\n\nLoads an experiment that was saved with save_experiment(). The loaded optimizer\nwill have the configuration and the objective function (thanks to dill).\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|----------|--------------|-------------------------------------|------------|\n| filename | [str](`str`) | Path to the experiment pickle file. | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|-----------|----------------------------------------------|---------------------------------------------------|\n| SpotOptim | [SpotOptim](`spotoptim.SpotOptim.SpotOptim`) | Loaded optimizer instance (without fun attached). |\n\n###### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|------------------------------------------|--------------------------------------|\n| | [FileNotFoundError](`FileNotFoundError`) | If the specified file doesn't exist. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#9487cd3a .cell execution_count=46}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n\n# Define experiment locally\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=15,\n n_initial=10,\n seed=42\n)\n\n# Save experiment (without results)\nopt.save_experiment(prefix=\"sphere_opt\")\n\n# On remote machine: load and run\nopt_remote = SpotOptim.load_experiment(\"sphere_opt_exp.pkl\")\nresult = opt_remote.optimize()\nopt_remote.save_result(prefix=\"sphere_opt\") # Save results\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nExperiment saved to sphere_opt_exp.pkl\nLoaded experiment from sphere_opt_exp.pkl\nExperiment saved to sphere_opt_res.pkl\nResult saved to sphere_opt_res.pkl\n```\n:::\n:::\n\n\n##### load_result { #spotoptim.SpotOptim.SpotOptim.load_result }\n\n```python\nSpotOptim.SpotOptim.load_result(filename)\n```\n\nLoad complete optimization results from a pickle file (suffix '_res.pkl')\n\nLoads results that were saved with save_result(). The loaded optimizer\nwill have both configuration and all optimization results.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|----------|--------------|---------------------------------|------------|\n| filename | [str](`str`) | Path to the result pickle file. | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|-----------|----------------------------------------------|--------------------------------------------------|\n| SpotOptim | [SpotOptim](`spotoptim.SpotOptim.SpotOptim`) | Loaded optimizer instance with complete results. |\n\n###### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|------------------------------------------|--------------------------------------|\n| | [FileNotFoundError](`FileNotFoundError`) | If the specified file doesn't exist. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#ac018cad .cell execution_count=47}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n\n# Run optimization\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=15,\n n_initial=5,\n seed=42\n)\nresult = opt.optimize()\n\n# Save complete results\nopt.save_result(prefix=\"sphere_opt\")\n\n# Load results\nopt = SpotOptim.load_result(\"sphere_opt_res.pkl\")\n\n# Analyze results\nprint(\"Best point:\", opt.best_x_)\nprint(\"Best value:\", opt.best_y_)\nprint(\"Total evaluations:\", opt.counter)\nprint(\"Success rate:\", opt.success_rate)\n\n# Continue optimization if needed\nopt.fun = lambda X: np.sum(X**2, axis=1) # Re-attach if continuing\nopt.max_iter = 50 # Increase budget\nresult = opt.optimize()\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nExperiment saved to sphere_opt_res.pkl\nResult saved to sphere_opt_res.pkl\nLoaded result from sphere_opt_res.pkl\nBest point: [-0.00041687 0.00031091]\nBest value: 2.70446473799289e-07\nTotal evaluations: 15\nSuccess rate: 0.8\n```\n:::\n:::\n\n\n##### map_to_factor_values { #spotoptim.SpotOptim.SpotOptim.map_to_factor_values }\n\n```python\nSpotOptim.SpotOptim.map_to_factor_values(X)\n```\n\nMap internal integer factor values back to string labels.\n\nFor factor variables, converts integer indices back to original string values.\nOther variable types remain unchanged.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-------------------------------------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Design points with integer values for factors, shape (n_samples, n_features). | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Design points with factor integers replaced by string labels. Dtype will be object or string if mixed types are present. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#50711290 .cell execution_count=48}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nimport numpy as np\nspot = SpotOptim(\n fun=sphere,\n bounds=[('red', 'blue'), (0, 10)]\n)\nspot.process_factor_bounds()\nX_int = np.array([[0, 5.0], [1, 8.0]])\nX_str = spot.map_to_factor_values(X_int)\nprint(X_str[0])\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n['red' 5.0]\n```\n:::\n:::\n\n\n##### mo2so { #spotoptim.SpotOptim.SpotOptim.mo2so }\n\n```python\nSpotOptim.SpotOptim.mo2so(y_mo)\n```\n\nConvert multi-objective values to single-objective.\n\nConverts multi-objective values to a single-objective value by applying a user-defined\nfunction from `fun_mo2so`. If no user-defined function is given, the\nvalues in the first objective column are used.\n\nThis method is called after the objective function evaluation. It returns a 1D array\nwith the single-objective values.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|----------------------------------------------------------------------------------------------------------|------------|\n| y_mo | [ndarray](`ndarray`) | If multi-objective, shape (n_samples, n_objectives). If single-objective, shape (n_samples,). | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|----------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Single-objective values, shape (n_samples,). |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#ac020776 .cell execution_count=49}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\n# Multi-objective function\ndef mo_fun(X):\n return np.column_stack([\n np.sum(X**2, axis=1),\n np.sum((X-1)**2, axis=1)\n ])\n\n# Example 1: Default behavior (use first objective)\nopt1 = SpotOptim(\n fun=mo_fun,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5\n)\ny_mo = np.array([[1.0, 2.0], [3.0, 4.0]])\ny_so = opt1.mo2so(y_mo)\nprint(f\"Single-objective (default): {y_so}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSingle-objective (default): [1. 3.]\n```\n:::\n:::\n\n\n::: {#896a3ac8 .cell execution_count=50}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n# Example 2: Custom conversion function (sum of objectives)\ndef custom_mo2so(y_mo):\n return y_mo[:, 0] + y_mo[:, 1]\n\nopt2 = SpotOptim(\n fun=mo_fun,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5,\n fun_mo2so=custom_mo2so\n)\ny_so_custom = opt2.mo2so(y_mo)\nprint(f\"Single-objective (custom): {y_so_custom}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSingle-objective (custom): [3. 7.]\n```\n:::\n:::\n\n\n##### modify_bounds_based_on_var_type { #spotoptim.SpotOptim.SpotOptim.modify_bounds_based_on_var_type }\n\n```python\nSpotOptim.SpotOptim.modify_bounds_based_on_var_type()\n```\n\nModify bounds based on variable types.\n\n###### Adjusts bounds for each dimension according to its var_type {.doc-section .doc-section-adjusts-bounds-for-each-dimension-according-to-its-vartype}\n\n* 'int': Ensures bounds are integers (ceiling for lower, floor for upper)\n* 'factor': Bounds already set to (0, n_levels-1) by process_factor_bounds\n* 'float': Explicitly converts bounds to float\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n###### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|--------------------------------------------|\n| | [ValueError](`ValueError`) | If an unsupported var_type is encountered. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#9bddf754 .cell execution_count=51}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nspot = SpotOptim(fun=lambda x: x, bounds=[(0.5, 10.5)], var_type=['int'])\nprint(spot.bounds)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[(1, 10)]\n```\n:::\n:::\n\n\n::: {#65968f50 .cell execution_count=52}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nspot = SpotOptim(fun=lambda x: x, bounds=[(0, 10)], var_type=['float'])\nprint(spot.bounds)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[(0.0, 10.0)]\n```\n:::\n:::\n\n\n##### optimize { #spotoptim.SpotOptim.SpotOptim.optimize }\n\n```python\nSpotOptim.SpotOptim.optimize(X0=None)\n```\n\nRun the optimization process.\n\n###### The optimization terminates when either {.doc-section .doc-section-the-optimization-terminates-when-either}\n\n* Total function evaluations reach max_iter (including initial design), OR\n* Runtime exceeds max_time minutes\n\nInput/Output Spaces:\n * Input X0: Expected in Natural Space (original scale, physical units).\n * Output result.x: Returned in Natural Space.\n * Output result.X: Returned in Natural Space.\n * Internal Optimization: Performed in Transformed and Mapped Space.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------------------------------------------------------------------------------------------|-----------|\n| X0 | [ndarray](`ndarray`) | Initial design points in Natural Space, shape (n_initial, n_features). If None, generates space-filling design. Defaults to None. | `None` |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|----------------|---------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| OptimizeResult | [OptimizeResult](`scipy.optimize.OptimizeResult`) | Optimization result with fields: * x: best point found in Natural Space * fun: best function value * nfev: number of function evaluations (including initial design) * nit: number of sequential optimization iterations (after initial design) * success: whether optimization succeeded * message: termination message indicating reason for stopping, including statistics (function value, iterations, evaluations) * X: all evaluated points in Natural Space * y: all function values |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#46ea84ee .cell execution_count=53}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n max_iter=20,\n seed=0,\n x0=np.array([0.0, 0.0]),\n verbose=True\n)\nresult = opt.optimize()\nprint(result.message.splitlines()[0])\nprint(\"Best point:\", result.x)\nprint(\"Best value:\", result.fun)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nStarting point x0 validated and processed successfully.\n Original scale: [0. 0.]\n Internal scale: [0. 0.]\nTensorBoard logging disabled\nIncluding 1 starting points from x0 in initial design.\nInitial best: f(x) = 0.000000\nIter 1 | Best: 0.000000 | Curr: 14.707944 | Rate: 0.00 | Evals: 30.0%\nIter 2 | Best: 0.000000 | Curr: 30.786083 | Rate: 0.00 | Evals: 35.0%\nIter 3 | Best: 0.000000 | Curr: 0.013878 | Rate: 0.00 | Evals: 40.0%\nIter 4 | Best: 0.000000 | Curr: 0.000686 | Rate: 0.00 | Evals: 45.0%\nIter 5 | Best: 0.000000 | Curr: 0.000277 | Rate: 0.00 | Evals: 50.0%\nIter 6 | Best: 0.000000 | Curr: 0.000001 | Rate: 0.00 | Evals: 55.0%\nIter 7 | Best: 0.000000 | Curr: 0.000001 | Rate: 0.00 | Evals: 60.0%\nIter 8 | Best: 0.000000 | Curr: 0.000001 | Rate: 0.00 | Evals: 65.0%\nIter 9 | Best: 0.000000 | Curr: 0.000001 | Rate: 0.00 | Evals: 70.0%\nIter 10 | Best: 0.000000 | Curr: 0.000001 | Rate: 0.00 | Evals: 75.0%\nIter 11 | Best: 0.000000 | Curr: 0.000001 | Rate: 0.00 | Evals: 80.0%\nIter 12 | Best: 0.000000 | Curr: 0.000001 | Rate: 0.00 | Evals: 85.0%\nIter 13 | Best: 0.000000 | Curr: 0.000001 | Rate: 0.00 | Evals: 90.0%\nIter 14 | Best: 0.000000 | Curr: 0.000001 | Rate: 0.00 | Evals: 95.0%\nIter 15 | Best: 0.000000 | Curr: 0.000001 | Rate: 0.00 | Evals: 100.0%\nOptimization terminated: maximum evaluations (20) reached\nBest point: [0. 0.]\nBest value: 0.0\n```\n:::\n:::\n\n\n##### optimize_acquisition_func { #spotoptim.SpotOptim.SpotOptim.optimize_acquisition_func }\n\n```python\nSpotOptim.SpotOptim.optimize_acquisition_func()\n```\n\nOptimize the acquisition function to find the next point to evaluate.\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | The optimized point(s). If acquisition_fun_return_size == 1, returns 1D array of shape (n_features,). If acquisition_fun_return_size > 1, returns 2D array of shape (N, n_features), where N is min(acquisition_fun_return_size, population_size). |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#968eb1f4 .cell execution_count=54}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n max_iter=10,\n seed=0,\n)\nopt.optimize()\nx_next = opt.suggest_next_infill_point()\nprint(\"Next point to evaluate:\", x_next)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nNext point to evaluate: [[0.14356455 0.03793884]]\n```\n:::\n:::\n\n\n##### plot_importance { #spotoptim.SpotOptim.SpotOptim.plot_importance }\n\n```python\nSpotOptim.SpotOptim.plot_importance(threshold=0.0, figsize=(10, 6))\n```\n\nPlot variable importance.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------|------------------|---------------------------------------------------|-----------|\n| threshold | [float](`float`) | Minimum importance percentage to include in plot. | `0.0` |\n| figsize | [tuple](`tuple`) | Figure size. | `(10, 6)` |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#c012d79a .cell execution_count=55}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\n\ndef test_func(X):\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=15,\n n_initial=5,\n seed=42\n)\nopt.optimize()\nopt.plot_importance()\n```\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim_files/figure-html/cell-56-output-1.png){width=789 height=523}\n:::\n:::\n\n\n##### plot_important_hyperparameter_contour { #spotoptim.SpotOptim.SpotOptim.plot_important_hyperparameter_contour }\n\n```python\nSpotOptim.SpotOptim.plot_important_hyperparameter_contour(\n max_imp=3,\n show=True,\n alpha=0.8,\n cmap='jet',\n num=100,\n add_points=True,\n grid_visible=True,\n contour_levels=30,\n figsize=(12, 10),\n)\n```\n\nPlot surrogate contours using spotoptim.plot.visualization.plot_important_hyperparameter_contour.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|----------------|------------------|----------------------------------------------------------|------------|\n| max_imp | [int](`int`) | The maximum number of important hyperparameters to plot. | `3` |\n| show | [bool](`bool`) | Whether to show the plot. | `True` |\n| alpha | [float](`float`) | The alpha value for the plot. | `0.8` |\n| cmap | [str](`str`) | The colormap to use. | `'jet'` |\n| num | [int](`int`) | The number of points to use for the plot. | `100` |\n| add_points | [bool](`bool`) | Whether to add points to the plot. | `True` |\n| grid_visible | [bool](`bool`) | Whether to show the grid. | `True` |\n| contour_levels | [int](`int`) | The number of contour levels to use. | `30` |\n| figsize | [tuple](`tuple`) | The size of the plot. | `(12, 10)` |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#08446b7d .cell execution_count=56}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\n\ndef test_func(X):\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\n# 2-D problem: max_imp must not exceed n_dim (2)\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=15,\n n_initial=5,\n seed=42\n)\nopt.optimize()\nopt.plot_important_hyperparameter_contour(max_imp=2)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nPlotting surrogate contours for top 2 most important parameters:\n x0: importance = 72.58% (type: float)\n x1: importance = 27.42% (type: float)\n\nGenerating 1 surrogate plots...\n Plotting x0 vs x1\n```\n:::\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim_files/figure-html/cell-57-output-2.png){width=1113 height=950}\n:::\n:::\n\n\n##### plot_parameter_scatter { #spotoptim.SpotOptim.SpotOptim.plot_parameter_scatter }\n\n```python\nSpotOptim.SpotOptim.plot_parameter_scatter(\n result=None,\n show=True,\n figsize=(12, 10),\n ylabel='Objective Value',\n cmap='viridis_r',\n show_correlation=False,\n log_y=False,\n)\n```\n\nPlot parameter distributions showing relationship between each parameter and objective.\n\nCreates a grid of scatter plots, one for each parameter dimension, showing how\nthe objective function value varies with each parameter. The best configuration\nis marked with a red star. Parameters with log-scale transformations (var_trans)\nare automatically displayed on a log x-axis.\n\nOptionally displays Spearman correlation coefficients in plot titles for\nsensitivity analysis. For factor (categorical) variables, correlation is not\ncomputed and they are displayed with discrete positions on the x-axis.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|------------------|---------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|---------------------|\n| result | [OptimizeResult](`scipy.optimize.OptimizeResult`) | Optimization result containing best parameters. If None, uses the best found values from self.best_x_ and self.best_y_. | `None` |\n| show | [bool](`bool`) | Whether to display the plot. Defaults to True. | `True` |\n| figsize | [tuple](`tuple`) | Figure size as (width, height). Defaults to (12, 10). | `(12, 10)` |\n| ylabel | [str](`str`) | Label for y-axis. Defaults to \"Objective Value\". | `'Objective Value'` |\n| cmap | [str](`str`) | Colormap for scatter plot. Defaults to \"viridis_r\". | `'viridis_r'` |\n| show_correlation | [bool](`bool`) | Whether to compute and display Spearman correlation coefficients in plot titles. Requires scipy. Defaults to False. | `False` |\n| log_y | [bool](`bool`) | Whether to use logarithmic scale for y-axis. Defaults to False. | `False` |\n\n###### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|---------------------------------------|\n| | [ValueError](`ValueError`) | If no optimization data is available. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#bd9b9cf7 .cell execution_count=57}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef objective(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(\n fun=objective,\n bounds=[(-5, 5), (-5, 5), (-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\", \"x2\", \"x3\"],\n max_iter=30,\n n_initial=10,\n seed=42\n)\nresult = opt.optimize()\n# Plot parameter distributions\nopt.plot_parameter_scatter(result)\n# Plot with custom settings\nopt.plot_parameter_scatter(result, cmap=\"plasma\", ylabel=\"Error\")\n```\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim_files/figure-html/cell-58-output-1.png){width=1143 height=949}\n:::\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim_files/figure-html/cell-58-output-2.png){width=1143 height=949}\n:::\n:::\n\n\n##### plot_progress { #spotoptim.SpotOptim.SpotOptim.plot_progress }\n\n```python\nSpotOptim.SpotOptim.plot_progress(\n show=True,\n log_y=False,\n figsize=(10, 6),\n ylabel='Objective Value',\n mo=False,\n)\n```\n\nPlot optimization progress using spotoptim.plot.visualization.plot_progress.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|---------|------------------|----------------------------------------------|---------------------|\n| show | [bool](`bool`) | Whether to show the plot. | `True` |\n| log_y | [bool](`bool`) | Whether to use a logarithmic y-axis. | `False` |\n| figsize | [tuple](`tuple`) | The size of the plot. | `(10, 6)` |\n| ylabel | [str](`str`) | The label for the y-axis. | `'Objective Value'` |\n| mo | [bool](`bool`) | Whether the optimization is multi-objective. | `False` |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#6cc38941 .cell execution_count=58}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\n\ndef test_func(X):\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=15,\n n_initial=5,\n seed=42\n)\nopt.optimize()\nopt.plot_progress()\n```\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim_files/figure-html/cell-59-output-1.png){width=949 height=565}\n:::\n:::\n\n\n##### plot_surrogate { #spotoptim.SpotOptim.SpotOptim.plot_surrogate }\n\n```python\nSpotOptim.SpotOptim.plot_surrogate(\n i=0,\n j=1,\n show=True,\n alpha=0.8,\n var_name=None,\n cmap='jet',\n num=100,\n vmin=None,\n vmax=None,\n add_points=True,\n grid_visible=True,\n contour_levels=30,\n figsize=(12, 10),\n)\n```\n\nPlot the surrogate model for two dimensions.\n\nDelegates to spotoptim.plot.visualization.plot_surrogate.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|----------------|------------------------------------------------------------------------|-------------------------------------------|------------|\n| i | [int](`int`) | The index of the first dimension. | `0` |\n| j | [int](`int`) | The index of the second dimension. | `1` |\n| show | [bool](`bool`) | Whether to show the plot. | `True` |\n| alpha | [float](`float`) | The alpha value for the plot. | `0.8` |\n| var_name | [Optional](`typing.Optional`)\\[[List](`typing.List`)\\[[str](`str`)\\]\\] | The names of the variables. | `None` |\n| cmap | [str](`str`) | The colormap to use. | `'jet'` |\n| num | [int](`int`) | The number of points to use for the plot. | `100` |\n| vmin | [Optional](`typing.Optional`)\\[[float](`float`)\\] | The minimum value for the plot. | `None` |\n| vmax | [Optional](`typing.Optional`)\\[[float](`float`)\\] | The maximum value for the plot. | `None` |\n| add_points | [bool](`bool`) | Whether to add points to the plot. | `True` |\n| grid_visible | [bool](`bool`) | Whether to show the grid. | `True` |\n| contour_levels | [int](`int`) | The number of contour levels to use. | `30` |\n| figsize | [tuple](`tuple`) | The size of the plot. | `(12, 10)` |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#08e6ddd7 .cell execution_count=59}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\n\ndef test_func(X):\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=15,\n n_initial=5,\n seed=42\n)\nopt.optimize()\nopt.plot_surrogate()\n```\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim_files/figure-html/cell-60-output-1.png){width=1113 height=950}\n:::\n:::\n\n\n##### print_best { #spotoptim.SpotOptim.SpotOptim.print_best }\n\n```python\nSpotOptim.SpotOptim.print_best(\n result=None,\n transformations=None,\n show_name=True,\n precision=4,\n)\n```\n\nPrint the best solution found during optimization.\n\nThis method displays the best hyperparameters and objective value in a\nformatted table. It supports custom transformations for parameters\n(e.g., converting log-scale values back to original scale).\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------------|---------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------|\n| result | [OptimizeResult](`scipy.optimize.OptimizeResult`) | Optimization result object from optimize(). If None, uses the stored best values from the optimizer. Defaults to None. | `None` |\n| transformations | list of callable | List of transformation functions to apply to each parameter. Each function takes a single value and returns the transformed value. Use None for parameters that don't need transformation. Length must match number of dimensions. Example: [None, None, lambda x: 10**x] to convert the 3rd parameter from log10 scale. Defaults to None. | `None` |\n| show_name | [bool](`bool`) | Whether to display variable names. If False, uses generic names like 'x0', 'x1', etc. Defaults to True. | `True` |\n| precision | [int](`int`) | Number of decimal places for floating point values. Defaults to 4. | `4` |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#1acdef00 .cell execution_count=60}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x1\", \"x2\"],\n max_iter=10,\n n_initial=5\n)\nresult = opt.optimize()\nopt.print_best(result)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n\nBest Solution Found:\n--------------------------------------------------\n x1: -0.8128\n x2: 0.7338\n Objective Value: 1.1991\n Total Evaluations: 10\n```\n:::\n:::\n\n\n##### print_results { #spotoptim.SpotOptim.SpotOptim.print_results }\n\n```python\nSpotOptim.SpotOptim.print_results(*args, **kwargs)\n```\n\nAlias for print(get_results_table()) for compatibility.\nPrints the table.\n\n##### process_factor_bounds { #spotoptim.SpotOptim.SpotOptim.process_factor_bounds }\n\n```python\nSpotOptim.SpotOptim.process_factor_bounds()\n```\n\nProcess `bounds` to handle factor variables.\n\nFor dimensions with tuple bounds (factor variables), creates internal\ninteger mappings and replaces bounds with (0, n_levels-1).\nStores mappings in `self._factor_maps`: {dim_idx: {int_val: str_val}}\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n###### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|------------------------------------|\n| | [ValueError](`ValueError`) | If bounds are invalidly formatted. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#be43440f .cell execution_count=61}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nspot = SpotOptim(fun=lambda x: x, bounds=[('red', 'green', 'blue'), (0, 10)])\nspot.process_factor_bounds()\nprint(f\"spot.bounds (should be [(0, 2), (0, 10)]): {spot.bounds}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nspot.bounds (should be [(0, 2), (0, 10)]): [(0, 2), (0, 10)]\n```\n:::\n:::\n\n\n##### reinitialize_components { #spotoptim.SpotOptim.SpotOptim.reinitialize_components }\n\n```python\nSpotOptim.SpotOptim.reinitialize_components()\n```\n\nReinitialize components that were excluded during pickling.\n\nThis method recreates the surrogate model and LHS sampler that were\nexcluded when saving an experiment or result.\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#f5e8e992 .cell execution_count=62}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n# Run experiment\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n var_name=[\"x\", \"y\"],\n verbose=True)\nopt.optimize()\nopt.save_experiment(\"sphere_opt_exp.pkl\")\nopt = SpotOptim.load_experiment(\"sphere_opt_exp.pkl\")\nopt.reinitialize_components()\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTensorBoard logging disabled\nInitial best: f(x) = 0.845257\nIter 1 | Best: 0.724447 | Rate: 1.00 | Evals: 30.0%\nIter 2 | Best: 0.611655 | Rate: 1.00 | Evals: 35.0%\nIter 3 | Best: 0.119332 | Rate: 1.00 | Evals: 40.0%\nIter 4 | Best: 0.119332 | Curr: 0.429535 | Rate: 0.75 | Evals: 45.0%\nIter 5 | Best: 0.012526 | Rate: 0.80 | Evals: 50.0%\nIter 6 | Best: 0.000222 | Rate: 0.83 | Evals: 55.0%\nIter 7 | Best: 0.000009 | Rate: 0.86 | Evals: 60.0%\nIter 8 | Best: 0.000005 | Rate: 0.88 | Evals: 65.0%\nIter 9 | Best: 0.000001 | Rate: 0.89 | Evals: 70.0%\nIter 10 | Best: 0.000001 | Rate: 0.90 | Evals: 75.0%\nIter 11 | Best: 0.000000 | Rate: 0.91 | Evals: 80.0%\nIter 12 | Best: 0.000000 | Rate: 0.92 | Evals: 85.0%\nIter 13 | Best: 0.000000 | Curr: 0.000000 | Rate: 0.85 | Evals: 90.0%\nIter 14 | Best: 0.000000 | Rate: 0.86 | Evals: 95.0%\nIter 15 | Best: 0.000000 | Curr: 0.000000 | Rate: 0.80 | Evals: 100.0%\nExperiment saved to sphere_opt_exp.pkl\nLoaded experiment from sphere_opt_exp.pkl\n```\n:::\n:::\n\n\n##### remove_nan { #spotoptim.SpotOptim.SpotOptim.remove_nan }\n\n```python\nSpotOptim.SpotOptim.remove_nan(X, y, stop_on_zero_return=True)\n```\n\nRemove rows where y contains NaN or inf values.\nUsed in the optimize() method after function evaluations.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|---------------------|----------------------|---------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Design matrix, shape (n_samples, n_features). | _required_ |\n| y | [ndarray](`ndarray`) | Objective values, shape (n_samples,). | _required_ |\n| stop_on_zero_return | [bool](`bool`) | If True, raise error when all values are removed. | `True` |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|------------------|-----------------------------------------------|\n| tuple | [tuple](`tuple`) | (X_clean, y_clean) with NaN/inf rows removed. |\n\n###### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|------------------------------------------------------------|\n| | [ValueError](`ValueError`) | If all values are NaN/inf and stop_on_zero_return is True. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#0bc515e9 .cell execution_count=63}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nX = np.array([[1, 2], [3, 4], [5, 6]])\ny = np.array([1.0, np.nan, np.inf])\nX_clean, y_clean = opt.remove_nan(X, y, stop_on_zero_return=False)\nprint(\"Clean X:\", X_clean)\nprint(\"Clean y:\", y_clean)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nClean X: [[1 2]]\nClean y: [1.]\n```\n:::\n:::\n\n\n##### repair_non_numeric { #spotoptim.SpotOptim.SpotOptim.repair_non_numeric }\n\n```python\nSpotOptim.SpotOptim.repair_non_numeric(X, var_type)\n```\n\nRound non-numeric values to integers based on variable type.\n\n###### This method applies rounding to variables that are not continuous {.doc-section .doc-section-this-method-applies-rounding-to-variables-that-are-not-continuous}\n\n* 'float': No rounding (continuous values)\n* 'int': Rounded to integers\n* 'factor': Rounded to integers (representing categorical values)\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|----------|----------------------|------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | X array with values to potentially round. | _required_ |\n| var_type | list of str | List with type information for each dimension. | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|---------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | X array with non-continuous values rounded to integers. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#c83afe75 .cell execution_count=64}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n var_type=['int', 'float'])\nX = np.array([[1.2, 2.5], [3.7, 4.1], [5.9, 6.8]])\nX_repaired = opt.repair_non_numeric(X, opt.var_type)\nprint(X_repaired)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[[1. 2.5]\n [4. 4.1]\n [6. 6.8]]\n```\n:::\n:::\n\n\n##### rm_initial_design_NA_values { #spotoptim.SpotOptim.SpotOptim.rm_initial_design_NA_values }\n\n```python\nSpotOptim.SpotOptim.rm_initial_design_NA_values(X0, y0)\n```\n\nRemove NaN/inf values from initial design evaluations.\n\nThis method filters out design points that returned NaN or inf values\nduring initial evaluation. Unlike the sequential optimization phase where\npenalties are applied, initial design points with invalid values are\nsimply removed.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-------------------------------------------------------------------------|------------|\n| X0 | [ndarray](`ndarray`) | Initial design points in internal scale, shape (n_samples, n_features). | _required_ |\n| y0 | [ndarray](`ndarray`) | Function values at X0, shape (n_samples,). | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| | [Tuple](`typing.Tuple`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`), [int](`int`)\\] | Tuple[ndarray, ndarray, int]: Filtered (X0, y0) with only finite values and the original count before filtering. X0 has shape (n_valid, n_features), y0 has shape (n_valid,), and the int is the original size. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#c032a952 .cell execution_count=65}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=10\n)\nX0 = np.array([[1, 2], [3, 4], [5, 6]])\ny0 = np.array([5.0, np.nan, np.inf])\nX0_clean, y0_clean, n_eval = opt.rm_initial_design_NA_values(X0, y0)\nprint(X0_clean.shape) # (1, 2)\nprint(y0_clean) # array([5.])\nprint(n_eval) # 3\n# All valid values - no filtering\nX0 = np.array([[1, 2], [3, 4]])\ny0 = np.array([5.0, 25.0])\nX0_clean, y0_clean, n_eval = opt.rm_initial_design_NA_values(X0, y0)\nprint(X0_clean.shape) # (2, 2)\nprint(n_eval) # 2\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(1, 2)\n[5.]\n3\n(2, 2)\n2\n```\n:::\n:::\n\n\n##### save_experiment { #spotoptim.SpotOptim.SpotOptim.save_experiment }\n\n```python\nSpotOptim.SpotOptim.save_experiment(\n filename=None,\n prefix='experiment',\n path=None,\n overwrite=True,\n unpickleables='all',\n verbosity=0,\n)\n```\n\nSave the experiment configuration to a pickle file (suffix '_exp.pkl') .\n\nAn experiment contains the optimizer configuration needed to run optimization,\nbut excludes the results. This is useful for defining experiments locally and\nexecuting them on remote machines.\n\n###### The experiment includes {.doc-section .doc-section-the-experiment-includes}\n\n* Bounds, variable types, variable names\n* Optimization parameters (max_iter, n_initial, etc.)\n* Surrogate and acquisition settings\n* Random seed\n\n###### The experiment excludes {.doc-section .doc-section-the-experiment-excludes}\n\n* Function evaluations (X_, y_)\n* Optimization results\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|---------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------|\n| filename | [str](`str`) | Filename for the experiment file. If None, generates from prefix. Defaults to None. | `None` |\n| prefix | [str](`str`) | Prefix for auto-generated filename. Defaults to \"experiment\". | `'experiment'` |\n| path | [str](`str`) | Directory path to save the file. If None, saves in current directory. Creates directory if it doesn't exist. Defaults to None. | `None` |\n| overwrite | [bool](`bool`) | If True, overwrites existing file. If False, raises error if file exists. Defaults to True. | `True` |\n| unpickleables | [str](`str`) | Components to exclude for pickling: * \"all\": Excludes surrogate, lhs_sampler, tb_writer (experiment only) * \"file_io\": Excludes only tb_writer (lighter exclusion) Defaults to \"all\". | `'all'` |\n| verbosity | [int](`int`) | Verbosity level (0=silent, 1=basic, 2=detailed). Defaults to 0. | `0` |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#eaa044c0 .cell execution_count=66}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n\n# Define experiment locally\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=15,\n n_initial=10,\n seed=42\n)\n\n# Save experiment (without results)\nopt.save_experiment(prefix=\"sphere_opt\")\n\n# On remote machine: load and run\nopt_remote = SpotOptim.load_experiment(\"sphere_opt_exp.pkl\")\nresult = opt_remote.optimize()\nopt_remote.save_result(prefix=\"sphere_opt\") # Save results\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nExperiment saved to sphere_opt_exp.pkl\nLoaded experiment from sphere_opt_exp.pkl\nExperiment saved to sphere_opt_res.pkl\nResult saved to sphere_opt_res.pkl\n```\n:::\n:::\n\n\n##### save_result { #spotoptim.SpotOptim.SpotOptim.save_result }\n\n```python\nSpotOptim.SpotOptim.save_result(\n filename=None,\n prefix='result',\n path=None,\n overwrite=True,\n verbosity=0,\n)\n```\n\nSave the complete optimization results to a pickle file.\n\nA result contains all information from a completed optimization run, including\nthe experiment configuration and all evaluation results. This is useful for\nsaving completed runs for later analysis.\n\n###### The result includes everything in an experiment plus {.doc-section .doc-section-the-result-includes-everything-in-an-experiment-plus}\n\n* All evaluated points (X_)\n* All function values (y_)\n* Best point and best value\n* Iteration count\n* Success rate statistics\n* Noise statistics (if applicable)\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------|----------------|--------------------------------------------------------------------------------------------------------------------------------|------------|\n| filename | [str](`str`) | Filename for the result file. If None, generates from prefix. Defaults to None. | `None` |\n| prefix | [str](`str`) | Prefix for auto-generated filename. Defaults to \"result\". | `'result'` |\n| path | [str](`str`) | Directory path to save the file. If None, saves in current directory. Creates directory if it doesn't exist. Defaults to None. | `None` |\n| overwrite | [bool](`bool`) | If True, overwrites existing file. If False, raises error if file exists. Defaults to True. | `True` |\n| verbosity | [int](`int`) | Verbosity level (0=silent, 1=basic, 2=detailed). Defaults to 0. | `0` |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#8a6232cb .cell execution_count=67}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n\n# Run optimization\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=30,\n n_initial=10,\n seed=42\n)\nresult = opt.optimize()\n\n# Save complete results\nopt.save_result(prefix=\"sphere_opt\")\n\n# Later: load and analyze\nopt_loaded = SpotOptim.load_result(\"sphere_opt_res.pkl\")\nprint(\"Best value:\", opt_loaded.best_y_)\nopt_loaded.plot_surrogate()\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nExperiment saved to sphere_opt_res.pkl\nResult saved to sphere_opt_res.pkl\nLoaded result from sphere_opt_res.pkl\nBest value: 7.179263647302644e-08\n```\n:::\n\n::: {.cell-output .cell-output-display}\n![](SpotOptim_files/figure-html/cell-68-output-2.png){width=1125 height=950}\n:::\n:::\n\n\n##### fit_select_best_cluster { #spotoptim.SpotOptim.SpotOptim.fit_select_best_cluster }\n\n```python\nSpotOptim.SpotOptim.fit_select_best_cluster(X, y, k)\n```\n\nSelects all points from the cluster with the smallest mean y value.\n\nThis method performs K-means clustering and selects all points from the\ncluster whose center corresponds to the best (smallest) mean objective\nfunction value.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Design points, shape (n_samples, n_features). | _required_ |\n| y | [ndarray](`ndarray`) | Function values at X, shape (n_samples,). | _required_ |\n| k | [int](`int`) | Number of clusters. | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|---------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| tuple | [Tuple](`typing.Tuple`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`)\\] | A tuple containing: * selected_X (ndarray): Selected design points from best cluster, shape (m, n_features). * selected_y (ndarray): Function values at selected points, shape (m,). |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#745b49f2 .cell execution_count=68}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_surrogate_points=5,\n selection_method='best')\nX = np.random.rand(100, 2)\ny = np.random.rand(100)\nX_sel, y_sel = opt.fit_select_best_cluster(X, y, 5)\nprint(f\"X_sel.shape: {X_sel.shape}\")\nprint(f\"y_sel.shape: {y_sel.shape}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nX_sel.shape: (28, 2)\ny_sel.shape: (28,)\n```\n:::\n:::\n\n\n##### fit_select_distant_points { #spotoptim.SpotOptim.SpotOptim.fit_select_distant_points }\n\n```python\nSpotOptim.SpotOptim.fit_select_distant_points(X, y, k)\n```\n\nSelects k points that are distant from each other using K-means clustering.\n\nThis method performs K-means clustering to find k clusters, then selects\nthe point closest to each cluster center. This ensures a space-filling\nsubset of points for surrogate model training.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Design points, shape (n_samples, n_features). | _required_ |\n| y | [ndarray](`ndarray`) | Function values at X, shape (n_samples,). | _required_ |\n| k | [int](`int`) | Number of points to select. | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|---------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| tuple | [Tuple](`typing.Tuple`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`)\\] | A tuple containing: * selected_X (ndarray): Selected design points, shape (k, n_features). * selected_y (ndarray): Function values at selected points, shape (k,). |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#42018da4 .cell execution_count=69}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_surrogate_points=5)\nX = np.random.rand(100, 2)\ny = np.random.rand(100)\nX_sel, y_sel = opt.fit_select_distant_points(X, y, 5)\nprint(X_sel.shape)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(5, 2)\n```\n:::\n:::\n\n\n##### select_new { #spotoptim.SpotOptim.SpotOptim.select_new }\n\n```python\nSpotOptim.SpotOptim.select_new(A, X, tolerance=0)\n```\n\nSelect rows from A that are not in X.\nUsed in suggest_next_infill_point() to avoid duplicate evaluations.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------|----------------------|------------------------------------------------|------------|\n| A | [ndarray](`ndarray`) | Array with new values. | _required_ |\n| X | [ndarray](`ndarray`) | Array with known values. | _required_ |\n| tolerance | [float](`float`) | Tolerance value for comparison. Defaults to 0. | `0` |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|---------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|\n| tuple | [Tuple](`typing.Tuple`)\\[[np](`numpy`).[ndarray](`numpy.ndarray`), [np](`numpy`).[ndarray](`numpy.ndarray`)\\] | A tuple containing: * ndarray: Array with unknown (new) values. * ndarray: Array with True if value is new, otherwise False. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#fa6b706e .cell execution_count=70}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nA = np.array([[1, 2], [3, 4], [5, 6]])\nX = np.array([[3, 4], [7, 8]])\nnew_A, is_new = opt.select_new(A, X)\nprint(\"New A:\", new_A)\nprint(\"Is new:\", is_new)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nNew A: [[1 2]\n [5 6]]\nIs new: [ True False True]\n```\n:::\n:::\n\n\n##### sensitivity_spearman { #spotoptim.SpotOptim.SpotOptim.sensitivity_spearman }\n\n```python\nSpotOptim.SpotOptim.sensitivity_spearman()\n```\n\nCompute and print Spearman correlation between parameters and objective values.\n\nThis method analyzes the sensitivity of the objective function to each\nhyperparameter by computing Spearman rank correlations. For categorical\n(factor) variables, correlation is not computed as they require visual\ninspection instead.\n\n###### The method automatically handles different parameter types {.doc-section .doc-section-the-method-automatically-handles-different-parameter-types}\n\n* Integer/float parameters: Direct correlation with objective values\n* Log-transformed parameters (log10, log, ln): Correlation in log-space\n* Factor (categorical) parameters: Skipped with informative message\n\n###### Significance levels {.doc-section .doc-section-significance-levels}\n\n* ***: p < 0.001 (highly significant)\n* **: p < 0.01 (significant)\n* *: p < 0.05 (marginally significant)\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#8d6643cc .cell execution_count=71}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\n\ndef test_func(X):\n # x0 has strong effect, x1 has weak effect\n X = np.atleast_2d(X)\n return 10 * X[:, 0]**2 + 0.1 * X[:, 1]**2\n\nopt = SpotOptim(\n fun=test_func,\n bounds=[(-5, 5), (-5, 5)],\n var_name=[\"x0\", \"x1\"],\n max_iter=15,\n n_initial=5,\n seed=42\n)\nopt.optimize()\nopt.sensitivity_spearman()\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n\nSensitivity Analysis (Spearman Correlation):\n--------------------------------------------------\n x0 : -0.011 (p=0.970)\n x1 : +0.029 (p=0.919)\n```\n:::\n:::\n\n\n###### Note {.doc-section .doc-section-note}\n\nOnly meaningful after optimize() has been called with sufficient evaluations.\n\n##### set_seed { #spotoptim.SpotOptim.SpotOptim.set_seed }\n\n```python\nSpotOptim.SpotOptim.set_seed()\n```\n\nSet global random seeds for reproducibility.\n\n###### Sets seeds for {.doc-section .doc-section-sets-seeds-for}\n\n* random\n* numpy.random\n* torch (cpu and cuda)\n\nOnly performs actions if self.seed is not None.\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#c586c2d4 .cell execution_count=72}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\nspot = SpotOptim(fun=lambda x: x, bounds=[(0, 1)], seed=42)\nspot.set_seed()\nnp.random.rand() # Should be deterministic\n```\n\n::: {.cell-output .cell-output-display execution_count=72}\n```\n0.3745401188473625\n```\n:::\n:::\n\n\n##### setup_dimension_reduction { #spotoptim.SpotOptim.SpotOptim.setup_dimension_reduction }\n\n```python\nSpotOptim.SpotOptim.setup_dimension_reduction()\n```\n\nSet up dimension reduction by identifying fixed dimensions.\n\nidentifies dimensions where lower and upper bounds are equal in **Transformed Space**.\nReduces `self.bounds`, `self.lower`, `self.upper`, etc., to the **Mapped Space**\n(active variables only).\n\nThe resulting `self.bounds` defines the **Transformed and Mapped Space** used\nfor optimization.\n\nThis method identifies variables that are fixed (constant) and excludes them\nfrom the optimization process. It stores:\n * Original bounds and metadata in `all_*` attributes\n * Boolean mask of fixed dimensions in `ident`\n * Reduced bounds, types, and names for optimization\n * `red_dim` flag indicating if reduction occurred\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#965ca936 .cell execution_count=73}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nspot = SpotOptim(fun=lambda x: x, bounds=[(1, 10), (5, 5), (0, 1)])\nspot.setup_dimension_reduction()\nprint(\"Original lower bounds:\", spot.all_lower)\nprint(\"Original upper bounds:\", spot.all_upper)\nprint(\"Fixed dimensions mask:\", spot.ident)\nprint(\"Reduced lower bounds:\", spot.lower)\nprint(\"Reduced upper bounds:\", spot.upper)\nprint(\"Reduced variable names:\", spot.var_name)\nprint(\"Is dimension reduction active?\", spot.red_dim)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nOriginal lower bounds: [1. 0.]\nOriginal upper bounds: [10. 1.]\nFixed dimensions mask: [False False]\nReduced lower bounds: [1. 0.]\nReduced upper bounds: [10. 1.]\nReduced variable names: ['x0', 'x2']\nIs dimension reduction active? False\n```\n:::\n:::\n\n\n##### store_mo { #spotoptim.SpotOptim.SpotOptim.store_mo }\n\n```python\nSpotOptim.SpotOptim.store_mo(y_mo)\n```\n\nStore multi-objective values in self.y_mo.\n\nIf multi-objective values are present (ndim==2), they are stored in self.y_mo.\nNew values are appended to existing ones. For single-objective problems,\nself.y_mo remains None.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|----------------------------------------------------------------------------------------------------------|------------|\n| y_mo | [ndarray](`ndarray`) | If multi-objective, shape (n_samples, n_objectives). If single-objective, shape (n_samples,). | _required_ |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#1bd0d04d .cell execution_count=74}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(\n fun=lambda X: np.column_stack([\n np.sum(X**2, axis=1),\n np.sum((X-1)**2, axis=1)\n ]),\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10,\n n_initial=5\n)\ny_mo_1 = np.array([[1.0, 2.0], [3.0, 4.0]])\nopt.store_mo(y_mo_1)\nprint(f\"y_mo after first call: {opt.y_mo}\")\ny_mo_2 = np.array([[5.0, 6.0], [7.0, 8.0]])\nopt.store_mo(y_mo_2)\nprint(f\"y_mo after second call: {opt.y_mo}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ny_mo after first call: [[1. 2.]\n [3. 4.]]\ny_mo after second call: [[1. 2.]\n [3. 4.]\n [5. 6.]\n [7. 8.]]\n```\n:::\n:::\n\n\n##### suggest_next_infill_point { #spotoptim.SpotOptim.SpotOptim.suggest_next_infill_point }\n\n```python\nSpotOptim.SpotOptim.suggest_next_infill_point()\n```\n\nSuggest next point to evaluate (dispatcher).\n\nThe returned point is in the **Transformed and Mapped Space** (Internal Optimization Space).\nThis means:\n1. Transformations (e.g., log, sqrt) have been applied.\n2. Dimension reduction has been applied (fixed variables removed).\n\nProcess:\n1. Try candidates from acquisition function optimizer.\n2. Handle acquisition failure (fallback).\n3. Return last attempt if all fails.\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|----------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Next point(s) to evaluate in **Transformed and Mapped Space**. |\n| | [np](`numpy`).[ndarray](`numpy.ndarray`) | Shape is (n_infill_points, n_features). |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#2b9628ff .cell execution_count=75}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n n_infill_points=2\n)\n# Need to initialize optimization state (X_, y_, surrogate)\n# Normally done inside optimize()\nnp.random.seed(0)\nopt.X_ = np.random.rand(10, 2)\nopt.y_ = np.random.rand(10)\nopt.fit_surrogate(opt.X_, opt.y_)\nx_next = opt.suggest_next_infill_point()\nx_next.shape\n```\n\n::: {.cell-output .cell-output-display execution_count=75}\n```\n(2, 2)\n```\n:::\n:::\n\n\n##### to_all_dim { #spotoptim.SpotOptim.SpotOptim.to_all_dim }\n\n```python\nSpotOptim.SpotOptim.to_all_dim(X_red)\n```\n\nExpand reduced-dimensional points to full-dimensional representation.\n\nThis method restores points from the reduced optimization space to the\nfull-dimensional space by inserting fixed values for constant dimensions.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-------------------------------------------------------------|------------|\n| X_red | [ndarray](`ndarray`) | Points in reduced space, shape (n_samples, n_reduced_dims). | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|-----------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Points in full space, shape (n_samples, n_original_dims). |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#ca7327c1 .cell execution_count=76}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n# Create problem with one fixed dimension\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (2, 2), (-5, 5)], # x1 is fixed at 2\n max_iter=10,\n n_initial=3\n)\nX_red = np.array([[1.0, 3.0], [2.0, 4.0]]) # Only x0 and x2\nX_full = opt.to_all_dim(X_red)\nprint(X_full.shape)\nprint(X_full[:, 1])\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(2, 3)\n[2. 2.]\n```\n:::\n:::\n\n\n##### to_red_dim { #spotoptim.SpotOptim.SpotOptim.to_red_dim }\n\n```python\nSpotOptim.SpotOptim.to_red_dim(X_full)\n```\n\nReduce full-dimensional points to optimization space.\n\nThis method removes fixed dimensions from full-dimensional points,\nextracting only the varying dimensions used in optimization.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------------------|------------|\n| X_full | [ndarray](`ndarray`) | Points in full space, shape (n_samples, n_original_dims). | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|-------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Points in reduced space, shape (n_samples, n_reduced_dims). |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#3165ce30 .cell execution_count=77}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n# Create problem with one fixed dimension\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (2, 2), (-5, 5)], # x1 is fixed at 2\n max_iter=10,\n n_initial=3\n)\nX_full = np.array([[1.0, 2.0, 3.0], [4.0, 2.0, 5.0]])\nX_red = opt.to_red_dim(X_full)\nprint(X_red.shape)\nprint(np.array_equal(X_red, np.array([[1.0, 3.0], [4.0, 5.0]])))\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(2, 2)\nTrue\n```\n:::\n:::\n\n\n##### transform_X { #spotoptim.SpotOptim.SpotOptim.transform_X }\n\n```python\nSpotOptim.SpotOptim.transform_X(X)\n```\n\nTransform parameter array from original to internal scale.\n\nConverts from **Natural Space** (Original) to **Transformed Space** (Full Dimension).\nDoes NOT handle dimension reduction (mapping).\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------------------|------------|\n| X | [ndarray](`ndarray`) | Array in **Natural Space**, shape (n_samples, n_features) | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|-------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Array in **Transformed Space** (Full Dimension) |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#6bbc2b80 .cell execution_count=78}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nspot = SpotOptim(fun=sphere, bounds=[(1, 10)])\nX_orig = np.array([[1], [10], [100]])\nspot.transform_X(X_orig)\n```\n\n::: {.cell-output .cell-output-display execution_count=78}\n```\narray([[ 1],\n [ 10],\n [100]])\n```\n:::\n:::\n\n\n##### transform_bounds { #spotoptim.SpotOptim.SpotOptim.transform_bounds }\n\n```python\nSpotOptim.SpotOptim.transform_bounds()\n```\n\nTransform bounds from original to internal scale.\n\nUpdates `self.bounds` (and `self.lower`, `self.upper`) from **Natural Space**\nto **Transformed Space**.\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#5e5e1304 .cell execution_count=79}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nimport numpy as np\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nspot = SpotOptim(fun=sphere, bounds=[(1, 10), (0.1, 100)])\nspot.var_trans = ['log10', 'sqrt']\nspot.transform_bounds()\nprint(spot.bounds)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[(0.0, 1.0), (0.31622776601683794, 10.0)]\n```\n:::\n:::\n\n\n##### transform_value { #spotoptim.SpotOptim.SpotOptim.transform_value }\n\n```python\nSpotOptim.SpotOptim.transform_value(x, trans)\n```\n\nApply transformation to a single float value.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|-----------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|\n| x | [float](`float`) | Value to transform | _required_ |\n| trans | [Optional](`typing.Optional`)\\[[str](`str`)\\] | Transformation name. Can be one of 'id', 'log10', 'log', 'ln', 'sqrt', 'exp', 'square', 'cube', 'inv', 'reciprocal', or None. Also supports dynamic strings like 'log(x)', 'sqrt(x)', 'pow(x, p)'. | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|------------------|-------------------|\n| | [float](`float`) | Transformed value |\n\n###### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|--------------------------------------------|\n| | [TypeError](`TypeError`) | If x is not a float. |\n| | [ValueError](`ValueError`) | If an unknown transformation is specified. |\n\n###### Notes {.doc-section .doc-section-notes}\n\nSee also inverse_transform_value.\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#542dd7f8 .cell execution_count=80}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\ndef sphere(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\nspot = SpotOptim(fun=sphere, bounds=[(1, 10)])\nspot.transform_value(10, 'log10')\nspot.transform_value(100, 'log(x)')\n```\n\n::: {.cell-output .cell-output-display execution_count=80}\n```\nnp.float64(4.605170185988092)\n```\n:::\n:::\n\n\n##### update_repeats_infill_points { #spotoptim.SpotOptim.SpotOptim.update_repeats_infill_points }\n\n```python\nSpotOptim.SpotOptim.update_repeats_infill_points(x_next)\n```\n\nRepeat infill point for noisy function evaluation.\n\nFor noisy objective functions (repeats_surrogate > 1), creates multiple\ncopies of the suggested point for repeated evaluation. Otherwise, returns\nthe point in 2D array format.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|----------------------------------------------|------------|\n| x_next | [ndarray](`ndarray`) | Next point to evaluate, shape (n_features,). | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|---------------------------------------------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Points to evaluate, shape (repeats_surrogate, n_features) or (1, n_features) if repeats_surrogate == 1. |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#3719b498 .cell execution_count=81}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere, noisy_sphere\n# Without repeats\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n repeats_surrogate=1\n)\nx_next = np.array([1.0, 2.0])\nx_repeated = opt.update_repeats_infill_points(x_next)\nprint(x_repeated.shape)\n\n# With repeats for noisy function\nopt_noisy = SpotOptim(\n fun=noisy_sphere,\n bounds=[(-5, 5), (-5, 5)],\n repeats_surrogate=3\n)\nx_next = np.array([1.0, 2.0])\nx_repeated = opt_noisy.update_repeats_infill_points(x_next)\nprint(x_repeated.shape)\n# All three copies should be identical\nnp.all(x_repeated[0] == x_repeated[1])\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n(1, 2)\n(3, 2)\n```\n:::\n\n::: {.cell-output .cell-output-display execution_count=81}\n```\nnp.True_\n```\n:::\n:::\n\n\n##### update_stats { #spotoptim.SpotOptim.SpotOptim.update_stats }\n\n```python\nSpotOptim.SpotOptim.update_stats()\n```\n\nUpdate optimization statistics.\n\n###### Updates various statistics related to the optimization progress {.doc-section .doc-section-updates-various-statistics-related-to-the-optimization-progress}\n\n* `min_y`: Minimum y value found so far\n* `min_X`: X value corresponding to minimum y\n* `counter`: Total number of function evaluations\n\nNote: `success_rate` is updated separately via `update_success_rate()` method,\nwhich is called after each batch of function evaluations.\n\nIf \"noise\" is True (`repeats_initial > 1` or `repeats_surrogate > 1`), additionally computes:\n * `mean_X`: Unique design points (aggregated from repeated evaluations)\n * `mean_y`: Mean y values per design point\n * `var_y`: Variance of y values per design point\n * `min_mean_X`: X value of the best mean y value\n * `min_mean_y`: Best mean y value\n * `min_var_y`: Variance of the best mean y value\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#fced3d57 .cell execution_count=82}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n# Without noise\nopt = SpotOptim(fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10, n_initial=5)\nopt.optimize()\nprint(\"SpotOptim stats without noise:\")\nprint(f\"opt.X_: {opt.X_}\")\nprint(f\"opt.y_: {opt.y_}\")\nprint(f\"opt.min_y: {opt.min_y}\")\nprint(f\"opt.min_X: {opt.min_X}\")\nprint(f\"opt.counter: {opt.counter}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSpotOptim stats without noise:\nopt.X_: [[-4.96303208e+00 -4.06181824e+00]\n [ 3.08562673e+00 -2.42623763e+00]\n [ 1.40291497e+00 3.33245968e+00]\n [-9.31229558e-01 3.96708018e-01]\n [-2.53180289e+00 2.16916955e+00]\n [-8.08924685e-01 4.27346845e-01]\n [-4.89445951e-01 3.21994494e-01]\n [-2.73575737e-01 2.85309176e-02]\n [-2.13813456e-01 -3.16791773e-02]\n [ 1.57084240e-02 1.79026440e-03]]\nopt.y_: [4.11300548e+01 1.54077214e+01 1.30734580e+01 1.02456574e+00\n 1.11153224e+01 8.36984471e-01 3.43237793e-01 7.56576971e-02\n 4.67197644e-02 2.49959630e-04]\nopt.min_y: 0.0002499596303890926\nopt.min_X: [0.01570842 0.00179026]\nopt.counter: 10\n```\n:::\n:::\n\n\n::: {#67046485 .cell execution_count=83}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import noisy_sphere\n# With noise\nopt_noise = SpotOptim(fun=noisy_sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5,\n repeats_surrogate=2,\n repeats_initial=2)\nopt_noise.optimize()\nprint(\"SpotOptim stats with noise:\")\nprint(f\"opt_noise.X_: {opt_noise.X_}\")\nprint(f\"opt_noise.y_: {opt_noise.y_}\")\nprint(f\"opt_noise.min_y: {opt_noise.min_y}\")\nprint(f\"opt_noise.min_X: {opt_noise.min_X}\")\nprint(f\"opt_noise.counter: {opt_noise.counter}\")\nprint(f\"opt_noise.mean_X: {opt_noise.mean_X}\")\nprint(f\"opt_noise.mean_y: {opt_noise.mean_y}\")\nprint(f\"opt_noise.var_y: {opt_noise.var_y}\")\nprint(f\"opt_noise.min_mean_X: {opt_noise.min_mean_X}\")\nprint(f\"opt_noise.min_mean_y: {opt_noise.min_mean_y}\")\nprint(f\"opt_noise.min_var_y: {opt_noise.min_var_y}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSpotOptim stats with noise:\nopt_noise.X_: [[-3.32242899 -3.63371149]\n [-3.32242899 -3.63371149]\n [ 0.87623558 3.05355755]\n [ 0.87623558 3.05355755]\n [ 1.97861035 -2.19568415]\n [ 1.97861035 -2.19568415]\n [-1.0299556 2.92786127]\n [-1.0299556 2.92786127]\n [ 4.62573438 0.22500429]\n [ 4.62573438 0.22500429]\n [-0.40385369 2.9656634 ]\n [-0.40385369 2.9656634 ]\n [ 1.79112397 -2.48582033]\n [ 1.79112397 -2.48582033]\n [ 1.93215027 -1.73385113]\n [ 1.93215027 -1.73385113]\n [ 1.1485493 -0.85758033]\n [ 1.1485493 -0.85758033]\n [ 0.68339923 -0.35547711]\n [ 0.68339923 -0.35547711]]\nopt_noise.y_: [24.21119596 24.15836453 9.99131931 10.26016016 8.65669913 8.6827672\n 9.66976502 9.76296267 21.49615701 21.72398101 9.08499836 9.00820702\n 9.32850487 9.34258127 6.69456771 6.56502564 2.13966258 1.9706971\n 0.49628142 0.73603017]\nopt_noise.min_y: 0.4962814200115207\nopt_noise.min_X: [ 0.68339923 -0.35547711]\nopt_noise.counter: 20\nopt_noise.mean_X: [[-3.32242899 -3.63371149]\n [-1.0299556 2.92786127]\n [-0.40385369 2.9656634 ]\n [ 0.68339923 -0.35547711]\n [ 0.87623558 3.05355755]\n [ 1.1485493 -0.85758033]\n [ 1.79112397 -2.48582033]\n [ 1.93215027 -1.73385113]\n [ 1.97861035 -2.19568415]\n [ 4.62573438 0.22500429]]\nopt_noise.mean_y: [24.18478024 9.71636384 9.04660269 0.6161558 10.12573974 2.05517984\n 9.33554307 6.62979668 8.66973317 21.61006901]\nopt_noise.var_y: [6.97789966e-04 2.17145039e-03 1.47422756e-03 1.43698661e-02\n 1.80688502e-02 7.13733398e-03 4.95362454e-05 4.19528726e-03\n 1.69886138e-04 1.29759436e-02]\nopt_noise.min_mean_X: [ 0.68339923 -0.35547711]\nopt_noise.min_mean_y: 0.6161557962920916\nopt_noise.min_var_y: 0.014369866088655908\n```\n:::\n:::\n\n\n##### update_storage { #spotoptim.SpotOptim.SpotOptim.update_storage }\n\n```python\nSpotOptim.SpotOptim.update_storage(X_new, y_new)\n```\n\nUpdate storage (`X_`, `y_`) with new evaluation points.\n\nAppends new design points and their function values to the storage arrays.\nPoints are converted from internal scale to original scale before storage.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|-----------------------------------------------------------------|------------|\n| X_new | [ndarray](`ndarray`) | New design points in internal scale, shape (n_new, n_features). | _required_ |\n| y_new | [ndarray](`ndarray`) | Function values at X_new, shape (n_new,). | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|--------|---------------|\n| | None | None |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#5d2585ab .cell execution_count=84}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n n_initial=5)\n# Initialize with some data\nopt.X_ = np.array([[1, 2], [3, 4]])\nopt.y_ = np.array([5.0, 25.0])\nprint(\"Initial storage:\")\nprint(opt.X_)\nprint(opt.y_)\n# Add new points\nX_new = np.array([[0, 1], [2, 3]])\ny_new = np.array([1.0, 13.0])\nopt.update_storage(X_new, y_new)\nprint(\"Updated storage:\")\nprint(opt.X_)\nprint(opt.y_)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nInitial storage:\n[[1 2]\n [3 4]]\n[ 5. 25.]\nUpdated storage:\n[[1 2]\n [3 4]\n [0 1]\n [2 3]]\n[ 5. 25. 1. 13.]\n```\n:::\n:::\n\n\n##### update_success_rate { #spotoptim.SpotOptim.SpotOptim.update_success_rate }\n\n```python\nSpotOptim.SpotOptim.update_success_rate(y_new)\n```\n\nUpdate the rolling success rate of the optimization process.\n\nA success is counted only if the new value is better (smaller) than the best\nfound y value so far. The success rate is calculated based on the last\n`window_size` successes.\n\nImportant: This method should be called BEFORE updating self.y_ to correctly\ntrack improvements against the previous best value.\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|----------------------|------------------------------------------------------------------|------------|\n| y_new | [ndarray](`ndarray`) | The new function values to consider for the success rate update. | _required_ |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#1901a100 .cell execution_count=85}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nopt = SpotOptim(fun=lambda X: np.sum(X**2, axis=1),\n bounds=[(-5, 5), (-5, 5)],\n max_iter=10, n_initial=5)\nprint(opt.success_rate)\nopt.X_ = np.array([[1, 2], [3, 4], [0, 1]])\nopt.y_ = np.array([5.0, 3.0, 2.0])\nopt.update_success_rate(np.array([1.5, 2.5]))\nprint(opt.success_rate)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n0.0\n0.5\n```\n:::\n:::\n\n\n##### validate_x0 { #spotoptim.SpotOptim.SpotOptim.validate_x0 }\n\n```python\nSpotOptim.SpotOptim.validate_x0(x0)\n```\n\nValidate and process starting point x0. Called in `__init__` and `optimize`.\n\n###### This method checks that x0 {.doc-section .doc-section-this-method-checks-that-x0}\n\n* Is a numpy array\n* Has the correct number of dimensions\n* Has values within bounds (in original scale)\n* Is properly transformed to internal scale\n\n###### Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------|-----------------------------------|----------------------------------|------------|\n| x0 | [array](`array`) - [like](`like`) | Starting point in original scale | _required_ |\n\n###### Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|---------|------------------------------------------|---------------------------------------------------------------------|\n| ndarray | [np](`numpy`).[ndarray](`numpy.ndarray`) | Validated and transformed x0 in internal scale, shape (n_features,) |\n\n###### Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|------------------|\n| | [ValueError](`ValueError`) | If x0 is invalid |\n\n###### Examples {.doc-section .doc-section-examples}\n\n::: {#12715f5f .cell execution_count=86}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n x0=np.array([1.0, 2.0])\n)\n# x0 is validated during initialization\n```\n:::\n\n\n### SpotOptimConfig { #spotoptim.SpotOptim.SpotOptimConfig }\n\n```python\nSpotOptim.SpotOptimConfig(\n bounds=None,\n max_iter=20,\n n_initial=10,\n surrogate=None,\n acquisition='y',\n var_type=None,\n var_name=None,\n var_trans=None,\n tolerance_x=None,\n max_time=np.inf,\n repeats_initial=1,\n repeats_surrogate=1,\n ocba_delta=0,\n tensorboard_log=False,\n tensorboard_path=None,\n tensorboard_clean=False,\n fun_mo2so=None,\n seed=None,\n verbose=False,\n warnings_filter='ignore',\n n_infill_points=1,\n max_surrogate_points=None,\n selection_method='distant',\n acquisition_failure_strategy='random',\n penalty=False,\n penalty_val=None,\n acquisition_fun_return_size=3,\n acquisition_optimizer='differential_evolution',\n restart_after_n=100,\n restart_inject_best=True,\n x0=None,\n de_x0_prob=0.1,\n tricands_fringe=False,\n prob_de_tricands=0.8,\n window_size=None,\n min_tol_metric='chebyshev',\n prob_surrogate=None,\n n_jobs=1,\n acquisition_optimizer_kwargs=None,\n args=(),\n kwargs=None,\n)\n```\n\nConfiguration parameters for SpotOptim.\n\n### SpotOptimState { #spotoptim.SpotOptim.SpotOptimState }\n\n```python\nSpotOptim.SpotOptimState(\n X_=None,\n y_=None,\n y_mo=None,\n best_x_=None,\n best_y_=None,\n n_iter_=0,\n counter=0,\n success_rate=0.0,\n success_counter=0,\n _success_history=list(),\n _zero_success_count=0,\n mean_X=None,\n mean_y=None,\n var_y=None,\n min_mean_X=None,\n min_mean_y=None,\n min_var_y=None,\n min_X=None,\n min_y=None,\n restarts_results_=list(),\n)\n```\n\nMutable state of the optimization process.\n\n", - "supporting": [ - "SpotOptim_files" - ], - "filters": [], - "includes": {} - } -} \ No newline at end of file diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-3-output-1.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-3-output-1.png deleted file mode 100644 index ad301b6b..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-3-output-1.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-47-output-1.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-47-output-1.png deleted file mode 100644 index 97e45974..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-47-output-1.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-48-output-1.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-48-output-1.png deleted file mode 100644 index 97e45974..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-48-output-1.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-48-output-2.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-48-output-2.png deleted file mode 100644 index 8c731d14..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-48-output-2.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-49-output-1.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-49-output-1.png deleted file mode 100644 index 7cd2eddd..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-49-output-1.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-49-output-2.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-49-output-2.png deleted file mode 100644 index 8c731d14..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-49-output-2.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-50-output-1.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-50-output-1.png deleted file mode 100644 index 7cd2eddd..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-50-output-1.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-50-output-2.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-50-output-2.png deleted file mode 100644 index c3b28fd2..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-50-output-2.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-51-output-1.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-51-output-1.png deleted file mode 100644 index 301340cf..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-51-output-1.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-51-output-2.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-51-output-2.png deleted file mode 100644 index c3b28fd2..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-51-output-2.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-52-output-1.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-52-output-1.png deleted file mode 100644 index 8c731d14..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-52-output-1.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-53-output-1.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-53-output-1.png deleted file mode 100644 index 97e45974..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-53-output-1.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-54-output-2.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-54-output-2.png deleted file mode 100644 index 8c731d14..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-54-output-2.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-55-output-1.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-55-output-1.png deleted file mode 100644 index 7cd2eddd..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-55-output-1.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-55-output-2.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-55-output-2.png deleted file mode 100644 index c3b28fd2..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-55-output-2.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-56-output-1.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-56-output-1.png deleted file mode 100644 index 302a8024..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-56-output-1.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-57-output-1.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-57-output-1.png deleted file mode 100644 index 8c731d14..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-57-output-1.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-57-output-2.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-57-output-2.png deleted file mode 100644 index 6c8ad1b4..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-57-output-2.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-58-output-1.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-58-output-1.png deleted file mode 100644 index ea3d2fc7..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-58-output-1.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-58-output-2.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-58-output-2.png deleted file mode 100644 index 4e382e21..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-58-output-2.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-59-output-1.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-59-output-1.png deleted file mode 100644 index 86b2aa4f..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-59-output-1.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-59-output-2.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-59-output-2.png deleted file mode 100644 index 73e39440..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-59-output-2.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-60-output-1.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-60-output-1.png deleted file mode 100644 index 6c8ad1b4..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-60-output-1.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-63-output-2.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-63-output-2.png deleted file mode 100644 index 73e39440..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-63-output-2.png and /dev/null differ diff --git a/_freeze/docs/reference/SpotOptim/figure-html/cell-68-output-2.png b/_freeze/docs/reference/SpotOptim/figure-html/cell-68-output-2.png deleted file mode 100644 index 08bb5f09..00000000 Binary files a/_freeze/docs/reference/SpotOptim/figure-html/cell-68-output-2.png and /dev/null differ diff --git a/_freeze/docs/slurm/execute-results/html.json b/_freeze/docs/slurm/execute-results/html.json deleted file mode 100644 index e89ab4e8..00000000 --- a/_freeze/docs/slurm/execute-results/html.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "hash": "ef6489c3419644ab4aa8aaec5fbe016f", - "result": { - "engine": "jupyter", - "markdown": "---\ntitle: \"Running spotoptim on the GWDG NHR Cluster (Slurm)\"\ndescription: \"End-to-end recipe for running a parallel spotoptim experiment on the GWDG NHR login node glogin-p3 with 16 CPUs / n_jobs=16.\"\nexecute:\n eval: false\n---\n\nThis chapter shows how to run a parallel `spotoptim` optimization on the\n[GWDG NHR](https://docs.hpc.gwdg.de/) cluster from the\n`glogin-p3.hpc.gwdg.de` login node, using the `standard96s:shared` CPU\npartition with 16 cores and `n_jobs=16`.\n\nThe flow has three phases:\n\n1. **Locally** — build a `SpotOptim` instance, freeze it with\n `save_experiment(...)`. The pickle holds the objective, bounds, surrogate,\n `n_jobs`, seed, and everything else needed to resume.\n2. **On the cluster** — `sbatch` a thin shell script that loads `uv`, then\n calls `SpotOptim.load_experiment(...)`, runs `optimize()`, and writes the\n result with `save_result(...)`.\n3. **Locally again** — `scp` the result back and analyse it with\n `SpotOptim.load_result(...)`.\n\n::: {.callout-note}\n### What changed compared to the spotpython workflow\n\nThe legacy `a_06_slurm.qmd` chapter (`spotpython`) needed two scripts on the\nremote machine: `startSlurm.sh` plus a `startPython.py` wrapper that called\n`load_and_run_spot_python_experiment(...)`. With `spotoptim` the wrapper is\nunnecessary: `SpotOptim.load_experiment(...)` and `opt.optimize()` are the\npublic API, and parallelism is configured by setting `n_jobs` on the\n`SpotOptim` constructor — there is no separate \"control\" object.\n:::\n\n## Prerequisites\n\n* SSH access to `glogin-p3.hpc.gwdg.de` as your project user `uxxxxx`\n (NHR account names follow the pattern `u` + five digits; your public key\n registered via [id.academiccloud.de](https://id.academiccloud.de/) →\n Security → SSH Public Keys). The login pattern follows the standard GWDG\n documentation — see\n [docs.hpc.gwdg.de/start_here/connecting](https://docs.hpc.gwdg.de/start_here/connecting/index.html).\n* A `~/.ssh/config` host alias makes the rest of the chapter copy-pastable:\n\n```bash\n# ~/.ssh/config (local machine)\nHost glogin-p3\n Hostname glogin-p3.hpc.gwdg.de\n User uxxxxx\n```\n\n## One-time cluster setup\n\nLog in once and clone the `spotoptim` repository under `$HOME/workspace`. The\nGWDG environment provides `uv` as a module, so no `conda` step is needed.\n\n```bash\nssh glogin-p3\nmkdir -p ~/workspace && cd ~/workspace\ngit clone https://github.com/sequential-parameter-optimization/spotoptim.git\ncd spotoptim\n\n# Compute nodes need an explicit proxy when downloading dependencies.\nexport http_proxy=http://www-cache.gwdg.de:3128\nexport https_proxy=http://www-cache.gwdg.de:3128\n\nmodule purge\nmodule load gcc uv\nuv python pin 3.13\nuv sync # creates .venv/, installs spotoptim editable\nuv run python -c \"from spotoptim import SpotOptim; print('ok')\"\n```\n\nAfter `uv sync` succeeds, `~/workspace/spotoptim/.venv/` is the environment\nthe Slurm script will activate via `uv run`. There is no per-job environment\nsetup — the lock file makes resync only-on-change.\n\n## Build the experiment locally\n\nCreate a `SpotOptim` instance with `n_jobs=16` and freeze it. The example\nuses the built-in 3-D `sphere` test function so that the chapter is\nreproducible without external data:\n\n::: {#e4934b46 .cell execution_count=1}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nPREFIX = \"a06\"\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5.0, 5.0)] * 3,\n n_initial=16, # one batch fills all 16 workers in parallel\n max_iter=80, # total evaluation budget (incl. the initial design)\n n_jobs=16, # process pool size on the compute node\n eval_batch_size=1, # set > 1 if the objective accepts a batch\n seed=0,\n verbose=True,\n)\n\nopt.save_experiment(prefix=PREFIX, path=\".\")\n# → writes ./a06_exp.pkl\n```\n:::\n\n\n`save_experiment` uses `dill` so that closures and lambdas survive the\nround-trip. The output file is named `_exp.pkl`. Replace the body of\n`fun=...` with any picklable callable to plug your own problem in.\n\n::: {.callout-tip}\n### `n_jobs` and `eval_batch_size`\n\n`n_jobs > 1` activates `optimize_steady_state()` — see\n[Parallel Optimization](optimize_parallel.qmd) for the full data flow. Use\n`-1` to mean \"all CPU cores on the worker node\". `eval_batch_size` collects\nthat many candidate points before a single dispatch to the pool, which is\nworth setting only when your objective natively handles batched input.\n:::\n\n## Copy the experiment to the cluster\n\n```bash\nssh glogin-p3 'mkdir -p ~/runs/spotoptim/logs'\nscp a06_exp.pkl glogin-p3:~/runs/spotoptim/\n```\n\n`~/runs/spotoptim/` is a convention used in this chapter; pick any\ndirectory — just keep `logs/` as a sub-directory because the Slurm script\nwrites its `.out`/`.err` files there.\n\n## The Slurm submission script\n\nThe repository ships a reference batch script at\n`scripts/slurm/run_spotoptim.sh`. Inline:\n\n```bash\n#!/bin/bash\n#SBATCH --job-name=spotoptim\n#SBATCH --partition=standard96s:shared\n#SBATCH --cpus-per-task=16\n#SBATCH --mem=16G\n#SBATCH --time=24:00:00\n#SBATCH --output=logs/spotoptim_%j.out\n#SBATCH --error=logs/spotoptim_%j.err\n#SBATCH --constraint=inet\n\nset -euo pipefail\nEXP_PKL=\"$1\"\n\n# GWDG proxy + thread pinning (one BLAS thread per worker process).\nexport http_proxy=http://www-cache.gwdg.de:3128\nexport https_proxy=http://www-cache.gwdg.de:3128\nexport OMP_NUM_THREADS=1\nexport OPENBLAS_NUM_THREADS=1\nexport MKL_NUM_THREADS=1\nexport PYTHONUNBUFFERED=1\n\nmkdir -p logs\nmodule purge 2>/dev/null || true\nmodule load gcc uv\n\ncd \"${SPOTOPTIM_REPO:-$HOME/workspace/spotoptim}\"\nuv run python scripts/slurm/run_spotoptim.py \"$EXP_PKL\"\n```\n\n::: {.callout-warning}\n### Why `OMP_NUM_THREADS=1` is mandatory\n\nThe 16 worker processes inherit `OMP_NUM_THREADS` from the batch\nenvironment. Without pinning, each worker would launch its own BLAS\nthread-pool of `cpu_count()` threads, leading to 16 × 16 = 256 threads on a\nshared node and severe contention. The graph-elites benchmark in\n`~/workspace/graph-elites/gwdg/slurm/run_spot_monet.sh` uses the same\npinning for the same reason.\n:::\n\nThe Python runner `scripts/slurm/run_spotoptim.py` is a 30-line wrapper:\n\n```python\nimport argparse\nfrom pathlib import Path\nfrom spotoptim import SpotOptim\n\np = argparse.ArgumentParser()\np.add_argument(\"exp_pkl\", type=Path)\nargs = p.parse_args()\n\nexp_path = args.exp_pkl.resolve()\nprefix = exp_path.name.removesuffix(\"_exp.pkl\")\n\nopt = SpotOptim.load_experiment(str(exp_path))\nresult = opt.optimize()\nopt.save_result(prefix=prefix, path=str(exp_path.parent))\n\nprint(f\"nfev={result.nfev} fun={result.fun:.6g} x={result.x}\")\n```\n\n`optimize()` honours the `n_jobs` value baked into the experiment, so the\nrunner itself never mentions parallelism — it does load → run → save.\n\n## Submit the job\n\n```bash\nssh glogin-p3\ncd ~/runs/spotoptim\nsbatch ~/workspace/spotoptim/scripts/slurm/run_spotoptim.sh \\\n ~/runs/spotoptim/a06_exp.pkl\n# → Submitted batch job 12345678\n```\n\nPass the experiment path as an absolute path; the Slurm script `cd`s into the\nspotoptim repo, so a relative path would resolve there instead of in\n`~/runs/spotoptim/`.\n\n::: {.callout-tip}\n### Faster scheduling for small jobs\n\nAdd `--qos=2h` to the `sbatch` call when your run fits in 2 hours; the\nhigh-priority QoS usually starts within minutes but rejects walltime > 2 h.\nOverride the time at submit-time, not in the script header:\n\n```bash\nsbatch --qos=2h --time=00:30:00 \\\n ~/workspace/spotoptim/scripts/slurm/run_spotoptim.sh ...\n```\n:::\n\n## Monitor the job\n\n```bash\nsqueue --me\nsacct -j --format=JobID,State,Elapsed,MaxRSS,ExitCode\ntail -f ~/runs/spotoptim/logs/spotoptim_.out\n```\n\nA successful run prints, near the end of the `.out` file:\n\n```\n=== spotoptim job ===\nJob ID : 12345678\nCPUs : 16\nMem : 16384 MB\n…\nnfev=80 fun=0.000123 x=[ 0.0089 -0.0083 0.0027]\n=== Job completed at … ===\n```\n\nIf you see `OUT_OF_MEMORY` from `sacct`, raise `--mem` (the budget should be\nroughly `n_jobs × 1 GB`; spotoptim's surrogate adds a small constant on top).\n\n## Copy the result back and analyse\n\n```bash\nscp glogin-p3:~/runs/spotoptim/a06_res.pkl .\n```\n\n::: {#01f7c637 .cell execution_count=2}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\n\nopt = SpotOptim.load_result(\"a06_res.pkl\")\n\nprint(\"best fun :\", opt.best_y_)\nprint(\"best x :\", opt.best_x_)\nprint(\"nfev :\", opt.X_.shape[0])\n\nopt.plot_progress(log_y=True)\n```\n:::\n\n\n`load_result` reinitialises the surrogate and the LHS sampler that were\nstripped before pickling, so all the analysis methods on `SpotOptim`\n(`plot_progress`, `print_results`, `get_importance`, …) work as if the\nexperiment had been run locally.\n\n## Slurm command reference\n\n| Command | Description |\n| :-- | :-- |\n| `sbatch run_spotoptim.sh _exp.pkl` | Submit a job that runs `optimize()` on the supplied pickle. |\n| `sbatch --qos=2h --time=02:00:00 …` | High-priority QoS; faster scheduling, max walltime 2 h. |\n| `squeue --me` | List your queued and running jobs. |\n| `sacct -j --format=JobID,State,Elapsed,MaxRSS,ExitCode` | Per-job accounting (use `MaxRSS` to right-size `--mem`). |\n| `scancel ` | Cancel a job. |\n| `sinfo -p standard96s:shared` | Node availability on the CPU shared partition. |\n| `module load gcc uv` | Load `gcc` (often a `uv` dependency) plus the `uv` module on the login or compute node. |\n| `scp file glogin-p3:~/runs/spotoptim/` | Copy a file to the cluster. |\n| `scp glogin-p3:~/runs/spotoptim/_res.pkl .` | Copy the result back. |\n| `show-quota` | Show your storage quotas (HOME, project, workspaces). |\n\n## See also\n\n* [Parallel Optimization](optimize_parallel.qmd) — internal control flow when\n `n_jobs > 1`.\n* [GWDG HPC documentation](https://docs.hpc.gwdg.de/) — partitions, QoS,\n module system, GPU partitions (`grete`).\n\n", - "supporting": [ - "slurm_files" - ], - "filters": [], - "includes": {} - } -} \ No newline at end of file diff --git a/_freeze/docs/spotoptim_init/execute-results/html.json b/_freeze/docs/spotoptim_init/execute-results/html.json index 7ad087e6..2f2dff50 100644 --- a/_freeze/docs/spotoptim_init/execute-results/html.json +++ b/_freeze/docs/spotoptim_init/execute-results/html.json @@ -1,8 +1,8 @@ { - "hash": "4d178b48285be17456d986951c7bfdc7", + "hash": "d0e4496842fa7e2784c603d8e32e3723", "result": { "engine": "jupyter", - "markdown": "---\ntitle: \"SpotOptim: `__init__()` method\"\ndescription: \"Step-by-step walkthrough of every action performed by SpotOptim.__init__(), with executable examples validated by pytest.\"\n---\n\nThis document explains every step performed inside `SpotOptim.__init__()` in\nthe order they occur.\nEach section has a `{python}` code block that can be executed directly and is\nvalidated by a corresponding pytest in `tests/`.\n\nRun all related tests with:\n\n```bash\nuv run pytest tests/test_spotoptim_deep.py tests/test_validate_x0.py tests/test_transform_bounds.py -v\n```\n\n---\n\n## Step 1 — Silence Warnings\n\n```python\nwarnings.filterwarnings(warnings_filter)\n```\n\nThe very first action is to apply a global Python `warnings` filter.\nThe default value is `\"ignore\"`, which suppresses deprecation and runtime\nwarnings from third-party libraries during optimization.\nAccepted values are any string accepted by `warnings.filterwarnings`:\n`\"ignore\"`, `\"always\"`, `\"error\"`, `\"once\"`, etc.\n\n::: {#76bf4f45 .cell execution_count=1}\n``` {.python .cell-code}\nimport warnings\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\n# \"always\" surfaces all warnings during a debugging session\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)], warnings_filter=\"always\")\nprint(f\"warnings_filter: {opt.warnings_filter}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nwarnings_filter: always\n```\n:::\n:::\n\n\n---\n\n## Step 2 — Machine Epsilon (`self.eps`)\n\n```python\nself.eps = np.sqrt(np.spacing(1))\n```\n\n`self.eps` is set to $\\sqrt{\\epsilon_{\\text{machine}}}$, approximately\n`1.49e-8` for IEEE 754 double precision.\nIt is used throughout the class as a tolerance for floating-point comparisons,\nfor example, when checking whether a starting point `x0` matches a fixed bound\nor when evaluating the `tolerance_x` stopping criterion.\n`np.spacing(1)` returns the smallest representable difference from 1.0\n(equivalent to `np.finfo(float).eps`).\n\n::: {#1cf44ef2 .cell execution_count=2}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nprint(f\"eps = {opt.eps:.6e}\")\nassert np.isclose(opt.eps, np.sqrt(np.spacing(1)))\nprint(\"eps check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\neps = 1.490116e-08\neps check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 3 — Default Tolerance (`tolerance_x`)\n\n```python\nif tolerance_x is None:\n tolerance_x = self.eps\n```\n\nIf the user does not supply `tolerance_x`, it defaults to `self.eps`.\n`tolerance_x` controls the minimum required improvement in the decision variable\nspace to keep the optimizer running: if consecutive best points are closer\nthan `tolerance_x` (measured by `min_tol_metric`, default `\"chebyshev\"`),\nthe run is considered converged.\n\n::: {#a6aa02f8 .cell execution_count=3}\n``` {.python .cell-code}\nopt_default = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nprint(f\"tolerance_x (default): {opt_default.tolerance_x:.6e}\")\nassert np.isclose(opt_default.tolerance_x, np.sqrt(np.spacing(1)))\n\nopt_custom = SpotOptim(fun=sphere, bounds=[(-5, 5)], tolerance_x=1e-3)\nprint(f\"tolerance_x (custom): {opt_custom.tolerance_x}\")\nassert opt_custom.tolerance_x == 1e-3\nprint(\"tolerance_x check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ntolerance_x (default): 1.490116e-08\ntolerance_x (custom): 0.001\ntolerance_x check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 4 — Parameter Inference from the Objective Function\n\n```python\nif bounds is None:\n bounds = getattr(fun, \"bounds\", None)\nif var_type is None:\n var_type = getattr(fun, \"var_type\", None)\nif var_name is None:\n var_name = getattr(fun, \"var_name\", None)\nif var_trans is None:\n var_trans = getattr(fun, \"var_trans\", None)\n```\n\nBefore any validation, `__init__` tries to read missing parameters directly\nfrom the callable `fun` using `getattr`.\nThis allows self-describing objective functions to carry their own metadata:\n`fun.bounds` as a list of `(lower, upper)` tuples, `fun.var_type` as a\nper-dimension type string list, `fun.var_name` as human-readable parameter\nnames, and `fun.var_trans` as a list of transformation strings.\n\n`bounds` is the only mandatory parameter: if it is `None` after this\ninference step, `__init__` raises a `ValueError` immediately. The remaining\nthree attributes (`var_type`, `var_name`, `var_trans`) are permitted to\nremain `None`; downstream steps supply their own defaults when needed.\n\n::: {#213ce463 .cell execution_count=4}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\nclass AnnotatedFun:\n bounds = [(-3, 3), (-3, 3)]\n var_name = [\"alpha\", \"beta\"]\n var_type = [\"float\", \"float\"]\n var_trans = [\"log10\", None]\n\n def __call__(self, X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n\nfun = AnnotatedFun()\nopt = SpotOptim(fun=fun, bounds=fun.bounds)\n\nprint(f\"var_name : {opt.var_name}\")\nprint(f\"var_type : {opt.var_type}\")\nassert opt.var_name == [\"alpha\", \"beta\"]\nprint(\"Parameter inference check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nvar_name : ['alpha', 'beta']\nvar_type : ['float', 'float']\nParameter inference check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 5 — Parameter Validation\n\n```python\nif max_iter < n_initial:\n raise ValueError(...)\nif n_jobs == -1:\n n_jobs = os.cpu_count() or 1\nelif n_jobs == 0 or n_jobs < -1:\n raise ValueError(...)\nif eval_batch_size < 1:\n raise ValueError(...)\nif acquisition_optimizer_kwargs is None:\n acquisition_optimizer_kwargs = {\"maxiter\": 10000, \"gtol\": 1e-9}\n```\n\nFour checks run before the configuration object is assembled. `max_iter`\nmust be at least `n_initial` because the total budget must accommodate the\ninitial design. `n_jobs` follows the scikit-learn convention: `-1` is\nresolved to `os.cpu_count()`, while `0` and values below `-1` are rejected.\n`eval_batch_size` controls how many candidate points accumulate before a\nsingle vectorised call is dispatched to the process pool and must be at\nleast `1`. Finally, when `acquisition_optimizer_kwargs` is not supplied it\nis initialised to `{\"maxiter\": 10000, \"gtol\": 1e-9}`, providing tight\nconvergence tolerances for the default differential-evolution run.\n\n::: {#5ebd2299 .cell execution_count=5}\n``` {.python .cell-code}\nimport os\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\n# valid: max_iter 20 >= n_initial 10\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)], max_iter=20, n_initial=10)\nprint(f\"max_iter={opt.max_iter}, n_initial={opt.n_initial} — valid.\")\n\n# invalid: max_iter < n_initial must raise\ntry:\n SpotOptim(fun=sphere, bounds=[(-5, 5)], max_iter=5, n_initial=10)\n raise AssertionError(\"Expected ValueError was not raised\")\nexcept ValueError as e:\n print(f\"Caught expected error: {e}\")\n\n# n_jobs=-1 resolves to all available CPU cores\nopt_p = SpotOptim(fun=sphere, bounds=[(-5, 5)], n_jobs=-1)\nassert opt_p.n_jobs == (os.cpu_count() or 1)\nprint(f\"n_jobs=-1 resolved to: {opt_p.n_jobs}\")\n\n# n_jobs=0 is rejected\ntry:\n SpotOptim(fun=sphere, bounds=[(-5, 5)], n_jobs=0)\n raise AssertionError(\"Expected ValueError was not raised\")\nexcept ValueError as e:\n print(f\"Caught expected error: {e}\")\n\n# acquisition_optimizer_kwargs default\nopt_aq = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nassert opt_aq.acquisition_optimizer_kwargs == {\"maxiter\": 10000, \"gtol\": 1e-9}\nprint(f\"acquisition_optimizer_kwargs (default): {opt_aq.acquisition_optimizer_kwargs}\")\nprint(\"Validation check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nmax_iter=20, n_initial=10 — valid.\nCaught expected error: max_iter (5) must be >= n_initial (10). max_iter represents the total function evaluation budget including initial design.\nn_jobs=-1 resolved to: 16\nCaught expected error: n_jobs must be a positive integer or -1 (all CPU cores), got 0.\nacquisition_optimizer_kwargs (default): {'maxiter': 10000, 'gtol': 1e-09}\nValidation check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 6 — `SpotOptimConfig` Construction\n\n```python\nself.config = SpotOptimConfig(\n bounds=bounds, max_iter=max_iter, n_initial=n_initial, ...\n)\n```\n\nAll constructor arguments are stored in a `SpotOptimConfig` dataclass-like\nobject assigned to `self.config`.\n`SpotOptim` uses a `__getattr__` proxy so that every field in `config` is also\naccessible directly on the `SpotOptim` instance — for example,\n`opt.n_initial` reads from `opt.config.n_initial`.\n`acquisition` is normalized to lowercase inside the config.\n\nStoring parameters in a separate object makes serialization and parameter\ninspection straightforward.\n\n::: {#6bacb3f2 .cell execution_count=6}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=20,\n n_initial=10,\n acquisition=\"EI\", # will be lowercased\n seed=99,\n)\n\nprint(f\"config type : {type(opt.config).__name__}\")\nprint(f\"acquisition : {opt.acquisition}\") # proxy via __getattr__\nprint(f\"seed : {opt.seed}\")\nassert opt.acquisition == \"ei\" # normalized to lowercase\nassert opt.n_initial == 10\nprint(\"Config construction check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nconfig type : SpotOptimConfig\nacquisition : ei\nseed : 99\nConfig construction check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 7 — `SpotOptimState` Construction\n\n```python\nself.state = SpotOptimState()\n```\n\n`SpotOptimState` holds all mutable runtime state that changes during\noptimization: evaluated points `X_`, function values `y_`, the current best\nsolution `best_x_`, `best_y_`, iteration counters, and similar fields.\nIt starts empty and is populated once `optimize()` is called.\n\nLike `config`, its attributes are accessible directly via `__getattr__`.\n\n::: {#c066ab4b .cell execution_count=7}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nprint(f\"state type: {type(opt.state).__name__}\")\n# Before optimizing, X_ and y_ do not exist yet\nassert not hasattr(opt.state, \"X_\") or opt.state.X_ is None or True\nprint(\"State construction check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nstate type: SpotOptimState\nState construction check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 8 — Objective Function and `objective_names`\n\n```python\nself.fun = fun\nself.objective_names = getattr(fun, \"objective_names\", getattr(fun, \"metrics\", None))\n```\n\nThe callable `fun` is stored as `self.fun`.\n`objective_names` is the list of output-metric names used by multi-objective\nor torch-based objectives (e.g., `[\"val_loss\", \"epochs\"]`).\nIt is copied from `fun.objective_names` or, as a fallback, from\n`fun.metrics`.\nIf neither attribute exists, `objective_names` is `None`.\n\nThe visualization module reads `optimizer.objective_names` to label plot axes.\n\n::: {#886bedaf .cell execution_count=8}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\n# Plain function — no objective_names\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nprint(f\"objective_names (plain): {opt.objective_names}\")\nassert opt.objective_names is None\n\n# Function with .objective_names attribute\nclass MetricFun:\n objective_names = [\"loss\"]\n def __call__(self, X):\n import numpy as np\n return np.sum(np.atleast_2d(X)**2, axis=1)\n\nopt2 = SpotOptim(fun=MetricFun(), bounds=[(-5, 5)])\nprint(f\"objective_names (annotated): {opt2.objective_names}\")\nassert opt2.objective_names == [\"loss\"]\nprint(\"objective_names check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nobjective_names (plain): None\nobjective_names (annotated): ['loss']\nobjective_names check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 9 — Random Number Generator (`rng`) and `set_seed()`\n\n```python\nself.rng = np.random.RandomState(self.seed)\nself.set_seed()\n```\n\nA `numpy.random.RandomState` object is created with the supplied `seed` and\nstored as `self.rng`.\nIt is used internally wherever random choices are needed (e.g., surrogate\nselection probabilities).\n\n`set_seed()` then calls `random.seed(self.seed)` and `np.random.seed(self.seed)`\nto seed Python's global and NumPy's global generators, providing a\nreproducibility guarantee across the full call stack.\n\n::: {#2142d80c .cell execution_count=9}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)], seed=42)\nprint(f\"rng type: {type(opt.rng).__name__}\")\n# Draw a sample — should be deterministic across invocations\nsample = opt.rng.uniform()\nprint(f\"First rng draw: {sample:.6f}\")\n\nopt2 = SpotOptim(fun=sphere, bounds=[(-5, 5)], seed=42)\nsample2 = opt2.rng.uniform()\nassert sample == sample2, \"RNG not reproducible with same seed\"\nprint(\"RNG reproducibility check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nrng type: RandomState\nFirst rng draw: 0.374540\nRNG reproducibility check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 10 — Factor Maps and Bounds Pre-processing\n\n```python\nself._factor_maps = {}\nself._original_bounds = self.bounds.copy()\nself.process_factor_bounds()\n```\n\n`self._factor_maps` is a dict mapping each dimension index to an\n`{int: str}` lookup table.\nIt is empty for purely numeric problems and populated by `process_factor_bounds()`\nwhen any bound is specified as a tuple of strings, e.g., `(\"red\", \"green\", \"blue\")`.\n\n`self._original_bounds` preserves the user-supplied bounds before any\ninteger-encoding transformation.\n\n`process_factor_bounds()` converts string-level bounds to their integer\nequivalents so the optimizer works in a uniform numeric space:\n\n```python\n# before: bounds = [(\"red\", \"green\", \"blue\"), (-5.0, 5.0)]\n# after: bounds = [(0, 2), (-5.0, 5.0)]\n# factor_maps = {0: {0: \"red\", 1: \"green\", 2: \"blue\"}}\n```\n\n::: {#9daf3d79 .cell execution_count=10}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\n# Numeric problem — factor_maps stays empty\nopt_num = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)])\nassert opt_num._factor_maps == {}\nprint(\"No factors: _factor_maps is empty.\")\n\n# Categorical dimension\nimport numpy as np\ndef cat_fun(X):\n X = np.atleast_2d(X)\n return X[:, 1].astype(float) ** 2\n\nopt_cat = SpotOptim(fun=cat_fun, bounds=[(\"red\", \"green\", \"blue\"), (-5.0, 5.0)])\nassert 0 in opt_cat._factor_maps\nassert opt_cat._factor_maps[0] == {0: \"red\", 1: \"green\", 2: \"blue\"}\nassert opt_cat.bounds[0] == (0, 2)\nprint(f\"factor_maps: {opt_cat._factor_maps}\")\nprint(\"Factor bounds check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nNo factors: _factor_maps is empty.\nfactor_maps: {0: {0: 'red', 1: 'green', 2: 'blue'}}\nFactor bounds check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 11 — Dimension Count (`n_dim`)\n\n```python\nself.n_dim = len(self.bounds)\n```\n\nAfter `process_factor_bounds()` has resolved all string-level dimensions,\n`n_dim` is calculated as the number of remaining bounds.\nFor problems with dimension reduction (fixed bounds, see Step 15),\n`n_dim` will later reflect the reduced dimension count.\n\n::: {#18618d8f .cell execution_count=11}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5), (-5, 5)])\nprint(f\"n_dim: {opt.n_dim}\")\nassert opt.n_dim == 3\nprint(\"n_dim check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nn_dim: 3\nn_dim check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 12 — Variable Type Detection (`detect_var_type`)\n\n```python\nif self.var_type is None:\n self.var_type = self.detect_var_type()\n```\n\n`detect_var_type()` infers the variable type for each dimension from the bounds.\nFor dimensions with purely numeric bounds it returns `\"float\"` — it does not\nauto-promote integer-looking bounds like `(0, 5)` to `\"int\"`.\nFactor dimensions created in Step 10 (string-tuple bounds) are already typed\nas `\"factor\"` and are left untouched.\n\nTo use integer variables you must pass `var_type=[\"int\", ...]` explicitly.\n\n::: {#b456294a .cell execution_count=12}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nimport numpy as np\n\n# Numeric bounds always yield float\nopt_f = SpotOptim(fun=sphere, bounds=[(-5.0, 5.0), (-5.0, 5.0)])\nprint(f\"var_type (floats) : {opt_f.var_type}\")\nassert all(t == \"float\" for t in opt_f.var_type)\n\n# Integer-looking bounds still yield float unless var_type is explicit\nopt_auto = SpotOptim(fun=sphere, bounds=[(0, 5), (0, 5)])\nprint(f\"var_type (no explicit) : {opt_auto.var_type}\")\nassert all(t == \"float\" for t in opt_auto.var_type)\n\n# Explicitly requesting int variables\nopt_int = SpotOptim(fun=sphere, bounds=[(0, 5), (0, 5)], var_type=[\"int\", \"int\"])\nprint(f\"var_type (explicit int) : {opt_int.var_type}\")\nassert all(t == \"int\" for t in opt_int.var_type)\n\n# Factor dim from string-tuple bounds\ndef cat_fun(X):\n X = np.atleast_2d(X)\n return X[:, 1].astype(float) ** 2\n\nopt_cat = SpotOptim(fun=cat_fun, bounds=[(\"a\", \"b\", \"c\"), (-5.0, 5.0)])\nprint(f\"var_type (factor + float) : {opt_cat.var_type}\")\nassert opt_cat.var_type[0] == \"factor\"\nassert opt_cat.var_type[1] == \"float\"\nprint(\"detect_var_type check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nvar_type (floats) : ['float', 'float']\nvar_type (no explicit) : ['float', 'float']\nvar_type (explicit int) : ['int', 'int']\nvar_type (factor + float) : ['factor', 'float']\ndetect_var_type check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 13 — Bound Modification (`modify_bounds_based_on_var_type`)\n\n```python\nself.modify_bounds_based_on_var_type()\n```\n\n`modify_bounds_based_on_var_type()` adjusts the bounds to be consistent with\nthe declared variable types.\nFor `\"int\"` and `\"factor\"` variables it ensures bounds are expressed as integers.\nFor `\"float\"` variables it converts bounds to `float`.\nThis makes downstream arithmetic type-safe and avoids mixed int/float arrays.\n\n::: {#b3f8191a .cell execution_count=13}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(0, 5), (-3.0, 3.0)], var_type=[\"int\", \"float\"])\nlower_types = [type(b[0]) for b in opt.bounds]\nprint(f\"Lower bound types: {lower_types}\")\n# int dimension lower bound should be int, float dimension should be float\nassert isinstance(opt.bounds[0][0], int)\nassert isinstance(opt.bounds[1][0], float)\nprint(\"modify_bounds_based_on_var_type check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nLower bound types: [, ]\nmodify_bounds_based_on_var_type check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 14 — Numpy Bound Arrays (`lower`, `upper`)\n\n```python\nself.lower = np.array([b[0] for b in self.bounds])\nself.upper = np.array([b[1] for b in self.bounds])\n```\n\nThe list-of-tuples `self.bounds` is unpacked into two numpy arrays:\n\n- `self.lower` — 1-D array of lower bounds, one entry per dimension\n- `self.upper` — 1-D array of upper bounds, one entry per dimension\n\nThese arrays are used throughout the class for vectorized bound arithmetic,\nfor example, when scaling Latin Hypercube samples — `lower + X_unit * (upper - lower)`.\n\n::: {#79777850 .cell execution_count=14}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (0, 10)])\nprint(f\"lower: {opt.lower}\")\nprint(f\"upper: {opt.upper}\")\nassert np.array_equal(opt.lower, [-5, 0])\nassert np.array_equal(opt.upper, [5, 10])\nprint(\"lower/upper arrays check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nlower: [-5. 0.]\nupper: [ 5. 10.]\nlower/upper arrays check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 15 — Default Variable Names\n\n```python\nif self.var_name is None:\n self.var_name = [f\"x{i}\" for i in range(self.n_dim)]\n```\n\nIf neither the constructor argument nor the function attribute provided names,\neach dimension receives an auto-generated name `\"x0\"`, `\"x1\"`, etc.\nNames appear in console output, error messages, and plot axis labels.\n\n::: {#68ca8e75 .cell execution_count=15}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt_auto = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)])\nprint(f\"var_name (auto): {opt_auto.var_name}\")\nassert opt_auto.var_name == [\"x0\", \"x1\"]\n\nopt_named = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], var_name=[\"lr\", \"wd\"])\nprint(f\"var_name (user): {opt_named.var_name}\")\nassert opt_named.var_name == [\"lr\", \"wd\"]\nprint(\"var_name check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nvar_name (auto): ['x0', 'x1']\nvar_name (user): ['lr', 'wd']\nvar_name check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 16 — Default Transformation Normalization (`handle_default_var_trans`)\n\n```python\nself.handle_default_var_trans()\n```\n\n`handle_default_var_trans()` normalizes the `var_trans` list without\napplying any transformation.\nIt replaces the strings `\"id\"` and `\"None\"` and Python `None` values with\n`None` so that all downstream code can use a single `None` check.\nIf `var_trans` was not provided, it initializes a list of `None` values\nwith length `n_dim`.\nIt also validates that the list length matches `n_dim`.\n\n::: {#623662db .cell execution_count=16}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt_none = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)])\nprint(f\"var_trans (default): {opt_none.var_trans}\")\nassert opt_none.var_trans == [None, None]\n\nopt_id = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], var_trans=[\"id\", \"None\"])\nprint(f\"var_trans (id/None): {opt_id.var_trans}\")\nassert all(t is None for t in opt_id.var_trans)\n\nopt_log = SpotOptim(fun=sphere, bounds=[(1, 100), (1, 100)], var_trans=[\"log10\", None])\nprint(f\"var_trans (log10): {opt_log.var_trans}\")\nassert opt_log.var_trans[0] == \"log10\"\nassert opt_log.var_trans[1] is None\nprint(\"handle_default_var_trans check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nvar_trans (default): [None, None]\nvar_trans (id/None): [None, None]\nvar_trans (log10): ['log10', None]\nhandle_default_var_trans check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 17 — Bound Snapshots and Transformation (`transform_bounds`)\n\n```python\nself._original_lower = self.lower.copy()\nself._original_upper = self.upper.copy()\nself.transform_bounds()\n```\n\nBefore any transformation is applied, `_original_lower` and `_original_upper`\nare created as copies of `self.lower` and `self.upper`.\nThese snapshots preserve the natural-space bounds and are used:\n\n- In `validate_x0()` to confirm that a starting point is within the original domain.\n- In reporting and visualization to display the problem in human-readable units.\n\n`transform_bounds()` then replaces `self.lower`, `self.upper`, and `self.bounds`\nwith the values in transformed space.\nFor example, with `var_trans=[\"log10\"]` and `bounds=[(1, 100)]`:\n\n- `_original_lower = [1]`, `_original_upper = [100]`\n- After transformation: `lower = [0.0]`, `upper = [2.0]`\n\n`transform_bounds()` also handles reversed bounds that arise from monotone\ndecreasing transforms such as `reciprocal`.\n\n::: {#334ebc97 .cell execution_count=17}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(1, 100), (1, 100)],\n var_trans=[\"log10\", \"log10\"],\n)\nprint(f\"_original_lower : {opt._original_lower}\")\nprint(f\"_original_upper : {opt._original_upper}\")\nprint(f\"lower (transformed) : {opt.lower}\")\nprint(f\"upper (transformed) : {opt.upper}\")\n\nassert np.isclose(opt._original_lower[0], 1.0)\nassert np.isclose(opt._original_upper[0], 100.0)\nassert np.isclose(opt.lower[0], np.log10(1)) # 0.0\nassert np.isclose(opt.upper[0], np.log10(100)) # 2.0\nprint(\"transform_bounds check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n_original_lower : [1. 1.]\n_original_upper : [100. 100.]\nlower (transformed) : [0. 0.]\nupper (transformed) : [2. 2.]\ntransform_bounds check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 18 — Dimension Reduction (`setup_dimension_reduction`)\n\n```python\nself.setup_dimension_reduction()\n```\n\n`setup_dimension_reduction()` identifies dimensions where `lower == upper`\n(fixed variables that carry no information for the optimizer).\nIt stores a boolean mask `self.ident` where `True` marks fixed dimensions.\n\nTwo sets of attributes coexist after this step:\n\n- `all_*` attributes (`all_lower`, `all_upper`, `all_var_name`, etc.) hold the\n full-dimensional representation including fixed dimensions.\n- The main attributes (`self.lower`, `self.upper`, `self.bounds`, `self.n_dim`)\n are reduced to the active (non-fixed) dimensions only.\n\nThe flag `self.red_dim` is `True` when at least one dimension was removed.\n\n::: {#6a277f7d .cell execution_count=18}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef fun3d(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n\n# dim 1 is fixed at 5.0\nopt = SpotOptim(fun=fun3d, bounds=[(-5, 5), (5, 5), (-5, 5)])\nprint(f\"red_dim : {opt.red_dim}\")\nprint(f\"ident mask : {opt.ident}\")\nprint(f\"n_dim (reduced) : {opt.n_dim}\")\nprint(f\"all_lower (full) : {opt.all_lower}\")\n\nassert opt.red_dim == True # numpy.bool_ — use == not `is`\nassert opt.ident[1] == True # numpy.bool_ — use == not `is`\nassert opt.n_dim == 2 # only 2 free dimensions\nassert len(opt.all_lower) == 3 # full-dim snapshot preserved\nprint(\"setup_dimension_reduction check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nred_dim : True\nident mask : [False True False]\nn_dim (reduced) : 2\nall_lower (full) : [-5. 5. -5.]\nsetup_dimension_reduction check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 19 — Starting Point Validation (`validate_x0`)\n\n```python\nif self.x0 is not None:\n self.x0 = self.validate_x0(self.x0)\n```\n\nWhen the user supplies `x0`, it is validated and transformed to internal scale\nbefore any optimization begins.\n`validate_x0()` performs the following checks:\n\n1. `x0` must be 1-D or 2-D (a scalar or 3-D array raises `ValueError`).\n2. The length of `x0` must match the full dimension count (before reduction).\n3. Each component must lie within its natural-space bounds.\n4. Fixed dimensions must exactly equal their fixed value.\n\nIf all checks pass, `validate_x0()` applies `transform_X()` to convert `x0`\nto the same internal (transformed and reduced) representation used during\noptimization.\n\n::: {#e579b5f5 .cell execution_count=19}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\n# x0 in natural scale, dim 1 is fixed\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (5, 5), (-5, 5)],\n x0=np.array([1.0, 5.0, 3.0]),\n)\nprint(f\"x0 (internal scale): {opt.x0}\")\n# After reduction, x0 should only contain the 2 free dims\nassert opt.x0.shape == (2,)\nprint(\"x0 = internal scale shape:\", opt.x0.shape)\n\n# x0 outside bounds must raise\ntry:\n SpotOptim(fun=sphere, bounds=[(-5, 5)], x0=np.array([10.0]))\n raise AssertionError(\"Should have raised ValueError\")\nexcept ValueError as e:\n print(f\"Caught: {e}\")\nprint(\"validate_x0 check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nx0 (internal scale): [1. 3.]\nx0 = internal scale shape: (2,)\nCaught: x0 (x0) = 10.0 is outside bounds [-5.0, 5.0]. \nvalidate_x0 check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 20 — Surrogate Initialization (`init_surrogate`)\n\n```python\nself.init_surrogate()\n```\n\n`init_surrogate()` sets up the surrogate model used to approximate the\nobjective function between evaluations.\nThree scenarios are handled:\n\n- `surrogate=None` (default): A `GaussianProcessRegressor` with a\n `ConstantKernel * Matern(nu=2.5)` kernel, 100 optimizer restarts,\n and `normalize_y=True` is created automatically.\n- `surrogate=[list]`: A multi-surrogate setup is configured.\n Probability weights (`self._prob_surrogate`) and per-surrogate\n `_max_surrogate_points_list` are computed.\n- `surrogate=`: The provided object is used as-is.\n\nAfter `init_surrogate()`, the following attributes are set:\n\n- `self.surrogate` — the active surrogate model\n- `self._surrogates_list` — list or `None`\n- `self._prob_surrogate` — selection probabilities or `None`\n- `self._max_surrogate_points_list` — per-surrogate max point limits\n- `self._active_max_surrogate_points` — current active limit\n\n::: {#e3b75489 .cell execution_count=20}\n``` {.python .cell-code}\nfrom sklearn.gaussian_process import GaussianProcessRegressor\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\n# Default surrogate\nopt_default = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nprint(f\"default surrogate type: {type(opt_default.surrogate).__name__}\")\nassert isinstance(opt_default.surrogate, GaussianProcessRegressor)\n\n# User-supplied surrogate\ncustom_gp = GaussianProcessRegressor(n_restarts_optimizer=5)\nopt_custom = SpotOptim(fun=sphere, bounds=[(-5, 5)], surrogate=custom_gp)\nassert opt_custom.surrogate is custom_gp\nprint(\"init_surrogate check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ndefault surrogate type: GaussianProcessRegressor\ninit_surrogate check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 21 — Latin Hypercube Sampler (`lhs_sampler`)\n\n```python\nself.lhs_sampler = LatinHypercube(d=self.n_dim, rng=self.seed)\n```\n\nA `scipy.stats.qmc.LatinHypercube` sampler is created for generating\nspace-filling initial designs.\n`d=self.n_dim` sets the dimensionality to the reduced dimension count.\n`rng=self.seed` seeds the sampler for reproducibility.\n\nThe primary parameter `rng=` is used instead of the legacy alias `seed=`\nto align with the current scipy API.\nThe sampler is called in `generate_initial_design()` via\n`self.lhs_sampler.random(n=self.n_initial)`, which returns points in `[0, 1]^d`\nthat are then scaled to `[lower, upper]`.\n\n::: {#b19037e1 .cell execution_count=21}\n``` {.python .cell-code}\nfrom scipy.stats.qmc import LatinHypercube\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nimport numpy as np\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], seed=42)\nprint(f\"lhs_sampler type: {type(opt.lhs_sampler).__name__}\")\nassert isinstance(opt.lhs_sampler, LatinHypercube)\n\n# Reproducibility: two samplers with rng=42 produce identical draws\ns1 = LatinHypercube(d=2, rng=42).random(5)\ns2 = LatinHypercube(d=2, rng=42).random(5)\nassert np.allclose(s1, s2)\nprint(\"lhs_sampler reproducibility check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nlhs_sampler type: LatinHypercube\nlhs_sampler reproducibility check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 22 — Window Size Default\n\n```python\nif self.window_size is None:\n if self.restart_after_n is not None:\n self.window_size = self.restart_after_n\n else:\n self.window_size = 100\n```\n\n`window_size` controls how many of the most recent evaluated points are\nincluded in the surrogate's training window each iteration\n(used together with `selection_method`).\n\nIf the user did not set `window_size` explicitly:\n\n- When `restart_after_n` is set, `window_size` mirrors it so that the\n sliding window aligns with the restart cycle length.\n- Otherwise it falls back to `100`, large enough to include all points\n in a typical short optimization run.\n\n::: {#37a42f75 .cell execution_count=22}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\n# Without restart — defaults to 100\nopt1 = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nprint(f\"window_size (no restart): {opt1.window_size}\")\nassert opt1.window_size == 100\n\n# With restart_after_n — matches it\nopt2 = SpotOptim(fun=sphere, bounds=[(-5, 5)], restart_after_n=40)\nprint(f\"window_size (restart=40): {opt2.window_size}\")\nassert opt2.window_size == 40\n\n# Explicit override\nopt3 = SpotOptim(fun=sphere, bounds=[(-5, 5)], window_size=25)\nprint(f\"window_size (explicit=25): {opt3.window_size}\")\nassert opt3.window_size == 25\nprint(\"window_size check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nwindow_size (no restart): 100\nwindow_size (restart=40): 40\nwindow_size (explicit=25): 25\nwindow_size check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 23 — TensorBoard Setup\n\n```python\nself._clean_tensorboard_logs()\nself._init_tensorboard_writer()\n```\n\nThe final two calls set up optional TensorBoard logging.\n\n`_clean_tensorboard_logs()` deletes existing log files at `tensorboard_path`\nif `tensorboard_clean=True` was requested.\n\n`_init_tensorboard_writer()` creates a `SummaryWriter` if `tensorboard_log=True`,\nwriting event files to `tensorboard_path`.\nWhen logging is disabled (the default), both methods are no-ops so there is\nno runtime cost.\n\n::: {#d2255245 .cell execution_count=23}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\n# Default: TensorBoard disabled — no writer created\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)], tensorboard_log=False)\nhas_writer = hasattr(opt, \"tb_writer\") and opt.tb_writer is not None\nprint(f\"tb_writer active: {has_writer} (expected False)\")\nassert not has_writer\nprint(\"TensorBoard no-op check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ntb_writer active: False (expected False)\nTensorBoard no-op check passed.\n```\n:::\n:::\n\n\n---\n\n## Complete Initialization Sequence Summary\n\n@tbl-init summarises every step performed by `__init__()` in order:\n\n| Step | Action | Key attributes set |\n|-----:|--------|-------------------|\n| 1 | `warnings.filterwarnings(warnings_filter)` | — |\n| 2 | Machine epsilon | `self.eps` |\n| 3 | `tolerance_x` default | `tolerance_x` |\n| 4 | Parameter inference from `fun` | `bounds`, `var_type`, `var_name`, `var_trans` |\n| 5 | Validate `max_iter`, `n_jobs`, `eval_batch_size`; default `acquisition_optimizer_kwargs` | — |\n| 6 | Create `SpotOptimConfig` | `self.config` |\n| 7 | Create `SpotOptimState` | `self.state` |\n| 8 | Store callable and `objective_names` | `self.fun`, `self.objective_names` |\n| 9 | Seed RNG | `self.rng` |\n| 10 | Factor maps, original bounds, `process_factor_bounds()` | `self._factor_maps`, `self._original_bounds`, `self.bounds` |\n| 11 | Compute `n_dim` | `self.n_dim` |\n| 12 | `detect_var_type()` | `self.var_type` |\n| 13 | `modify_bounds_based_on_var_type()` | `self.bounds` |\n| 14 | Unpack bounds to arrays | `self.lower`, `self.upper` |\n| 15 | Default variable names | `self.var_name` |\n| 16 | `handle_default_var_trans()` | `self.var_trans` |\n| 17 | Snapshot bounds, `transform_bounds()` | `self._original_lower`, `self._original_upper`, `self.bounds` |\n| 18 | `setup_dimension_reduction()` | `self.ident`, `self.red_dim`, `all_*` attributes |\n| 19 | `validate_x0()` | `self.x0` (transformed, reduced) |\n| 20 | `init_surrogate()` | `self.surrogate`, `self._surrogates_list` |\n| 21 | Create `lhs_sampler` | `self.lhs_sampler` |\n| 22 | `window_size` default | `self.window_size` |\n| 23 | TensorBoard setup | `self.tb_writer` |\n\n: Complete Initialization Sequence Summary {#tbl-init}\n\n", + "markdown": "---\ntitle: \"SpotOptim: `__init__()` method\"\ndescription: \"Step-by-step walkthrough of every action performed by SpotOptim.__init__(), with executable examples validated by pytest.\"\n---\n\nThis document explains every step performed inside `SpotOptim.__init__()` in\nthe order they occur.\nEach section has a `{python}` code block that can be executed directly and is\nvalidated by a corresponding pytest in `tests/`.\n\nRun all related tests with:\n\n```bash\nuv run pytest tests/test_spotoptim_deep.py tests/test_validate_x0.py tests/test_transform_bounds.py -v\n```\n\n---\n\n## Step 1 — Silence Warnings\n\n```python\nwarnings.filterwarnings(warnings_filter)\n```\n\nThe very first action is to apply a global Python `warnings` filter.\nThe default value is `\"ignore\"`, which suppresses deprecation and runtime\nwarnings from third-party libraries during optimization.\nAccepted values are any string accepted by `warnings.filterwarnings`:\n`\"ignore\"`, `\"always\"`, `\"error\"`, `\"once\"`, etc.\n\n::: {#3474b796 .cell execution_count=1}\n``` {.python .cell-code}\nimport warnings\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\n# \"always\" surfaces all warnings during a debugging session\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)], warnings_filter=\"always\")\nprint(f\"warnings_filter: {opt.warnings_filter}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nwarnings_filter: always\n```\n:::\n:::\n\n\n---\n\n## Step 2 — Machine Epsilon (`self.eps`)\n\n```python\nself.eps = np.sqrt(np.spacing(1))\n```\n\n`self.eps` is set to $\\sqrt{\\epsilon_{\\text{machine}}}$, approximately\n`1.49e-8` for IEEE 754 double precision.\nIt is used throughout the class as a tolerance for floating-point comparisons,\nfor example, when checking whether a starting point `x0` matches a fixed bound\nor when evaluating the `tolerance_x` stopping criterion.\n`np.spacing(1)` returns the smallest representable difference from 1.0\n(equivalent to `np.finfo(float).eps`).\n\n::: {#994d3e2f .cell execution_count=2}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nprint(f\"eps = {opt.eps:.6e}\")\nassert np.isclose(opt.eps, np.sqrt(np.spacing(1)))\nprint(\"eps check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\neps = 1.490116e-08\neps check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 3 — Default Tolerance (`tolerance_x`)\n\n```python\nif tolerance_x is None:\n tolerance_x = self.eps\n```\n\nIf the user does not supply `tolerance_x`, it defaults to `self.eps`.\n`tolerance_x` controls the minimum required improvement in the decision variable\nspace to keep the optimizer running: if consecutive best points are closer\nthan `tolerance_x` (measured by `min_tol_metric`, default `\"chebyshev\"`),\nthe run is considered converged.\n\n::: {#29d309ba .cell execution_count=3}\n``` {.python .cell-code}\nopt_default = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nprint(f\"tolerance_x (default): {opt_default.tolerance_x:.6e}\")\nassert np.isclose(opt_default.tolerance_x, np.sqrt(np.spacing(1)))\n\nopt_custom = SpotOptim(fun=sphere, bounds=[(-5, 5)], tolerance_x=1e-3)\nprint(f\"tolerance_x (custom): {opt_custom.tolerance_x}\")\nassert opt_custom.tolerance_x == 1e-3\nprint(\"tolerance_x check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ntolerance_x (default): 1.490116e-08\ntolerance_x (custom): 0.001\ntolerance_x check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 4 — Parameter Inference from the Objective Function\n\n```python\nif bounds is None:\n bounds = getattr(fun, \"bounds\", None)\nif var_type is None:\n var_type = getattr(fun, \"var_type\", None)\nif var_name is None:\n var_name = getattr(fun, \"var_name\", None)\nif var_trans is None:\n var_trans = getattr(fun, \"var_trans\", None)\n```\n\nBefore any validation, `__init__` tries to read missing parameters directly\nfrom the callable `fun` using `getattr`.\nThis allows self-describing objective functions to carry their own metadata:\n`fun.bounds` as a list of `(lower, upper)` tuples, `fun.var_type` as a\nper-dimension type string list, `fun.var_name` as human-readable parameter\nnames, and `fun.var_trans` as a list of transformation strings.\n\n`bounds` is the only mandatory parameter: if it is `None` after this\ninference step, `__init__` raises a `ValueError` immediately. The remaining\nthree attributes (`var_type`, `var_name`, `var_trans`) are permitted to\nremain `None`; downstream steps supply their own defaults when needed.\n\n::: {#649a6895 .cell execution_count=4}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\nclass AnnotatedFun:\n bounds = [(-3, 3), (-3, 3)]\n var_name = [\"alpha\", \"beta\"]\n var_type = [\"float\", \"float\"]\n var_trans = [\"log10\", None]\n\n def __call__(self, X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n\nfun = AnnotatedFun()\nopt = SpotOptim(fun=fun, bounds=fun.bounds)\n\nprint(f\"var_name : {opt.var_name}\")\nprint(f\"var_type : {opt.var_type}\")\nassert opt.var_name == [\"alpha\", \"beta\"]\nprint(\"Parameter inference check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nvar_name : ['alpha', 'beta']\nvar_type : ['float', 'float']\nParameter inference check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 5 — Parameter Validation\n\n```python\nif max_iter < n_initial:\n raise ValueError(...)\nif acquisition_optimizer_kwargs is None:\n acquisition_optimizer_kwargs = {\"maxiter\": 10000, \"gtol\": 1e-9}\n```\n\nTwo checks run before the configuration object is assembled. `max_iter`\nmust be at least `n_initial` because the total budget must accommodate the\ninitial design. When `acquisition_optimizer_kwargs` is not supplied it\nis initialised to `{\"maxiter\": 10000, \"gtol\": 1e-9}`, providing tight\nconvergence tolerances for the default differential-evolution run.\n\n::: {#4a234b77 .cell execution_count=5}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\n# valid: max_iter 20 >= n_initial 10\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)], max_iter=20, n_initial=10)\nprint(f\"max_iter={opt.max_iter}, n_initial={opt.n_initial} — valid.\")\n\n# invalid: max_iter < n_initial must raise\ntry:\n SpotOptim(fun=sphere, bounds=[(-5, 5)], max_iter=5, n_initial=10)\n raise AssertionError(\"Expected ValueError was not raised\")\nexcept ValueError as e:\n print(f\"Caught expected error: {e}\")\n\n# acquisition_optimizer_kwargs default\nopt_aq = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nassert opt_aq.acquisition_optimizer_kwargs == {\"maxiter\": 10000, \"gtol\": 1e-9}\nprint(f\"acquisition_optimizer_kwargs (default): {opt_aq.acquisition_optimizer_kwargs}\")\nprint(\"Validation check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nmax_iter=20, n_initial=10 — valid.\nCaught expected error: max_iter (5) must be >= n_initial (10). max_iter represents the total function evaluation budget including initial design.\nacquisition_optimizer_kwargs (default): {'maxiter': 10000, 'gtol': 1e-09}\nValidation check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 6 — `SpotOptimConfig` Construction\n\n```python\nself.config = SpotOptimConfig(\n bounds=bounds, max_iter=max_iter, n_initial=n_initial, ...\n)\n```\n\nAll constructor arguments are stored in a `SpotOptimConfig` dataclass-like\nobject assigned to `self.config`.\n`SpotOptim` uses a `__getattr__` proxy so that every field in `config` is also\naccessible directly on the `SpotOptim` instance — for example,\n`opt.n_initial` reads from `opt.config.n_initial`.\n`acquisition` is normalized to lowercase inside the config.\n\nStoring parameters in a separate object makes serialization and parameter\ninspection straightforward.\n\n::: {#76a2f008 .cell execution_count=6}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=20,\n n_initial=10,\n acquisition=\"EI\", # will be lowercased\n seed=99,\n)\n\nprint(f\"config type : {type(opt.config).__name__}\")\nprint(f\"acquisition : {opt.acquisition}\") # proxy via __getattr__\nprint(f\"seed : {opt.seed}\")\nassert opt.acquisition == \"ei\" # normalized to lowercase\nassert opt.n_initial == 10\nprint(\"Config construction check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nconfig type : SpotOptimConfig\nacquisition : ei\nseed : 99\nConfig construction check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 7 — `SpotOptimState` Construction\n\n```python\nself.state = SpotOptimState()\n```\n\n`SpotOptimState` holds all mutable runtime state that changes during\noptimization: evaluated points `X_`, function values `y_`, the current best\nsolution `best_x_`, `best_y_`, iteration counters, and similar fields.\nIt starts empty and is populated once `optimize()` is called.\n\nLike `config`, its attributes are accessible directly via `__getattr__`.\n\n::: {#e62fd3a1 .cell execution_count=7}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nprint(f\"state type: {type(opt.state).__name__}\")\n# Before optimizing, X_ and y_ do not exist yet\nassert not hasattr(opt.state, \"X_\") or opt.state.X_ is None or True\nprint(\"State construction check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nstate type: SpotOptimState\nState construction check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 8 — Objective Function and `objective_names`\n\n```python\nself.fun = fun\nself.objective_names = getattr(fun, \"objective_names\", getattr(fun, \"metrics\", None))\n```\n\nThe callable `fun` is stored as `self.fun`.\n`objective_names` is the list of output-metric names used by multi-objective\nor torch-based objectives (e.g., `[\"val_loss\", \"epochs\"]`).\nIt is copied from `fun.objective_names` or, as a fallback, from\n`fun.metrics`.\nIf neither attribute exists, `objective_names` is `None`.\n\nThe visualization module reads `optimizer.objective_names` to label plot axes.\n\n::: {#43549996 .cell execution_count=8}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\n# Plain function — no objective_names\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nprint(f\"objective_names (plain): {opt.objective_names}\")\nassert opt.objective_names is None\n\n# Function with .objective_names attribute\nclass MetricFun:\n objective_names = [\"loss\"]\n def __call__(self, X):\n import numpy as np\n return np.sum(np.atleast_2d(X)**2, axis=1)\n\nopt2 = SpotOptim(fun=MetricFun(), bounds=[(-5, 5)])\nprint(f\"objective_names (annotated): {opt2.objective_names}\")\nassert opt2.objective_names == [\"loss\"]\nprint(\"objective_names check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nobjective_names (plain): None\nobjective_names (annotated): ['loss']\nobjective_names check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 9 — Random Number Generator (`rng`) and `set_seed()`\n\n```python\nself.rng = np.random.RandomState(self.seed)\nself.set_seed()\n```\n\nA `numpy.random.RandomState` object is created with the supplied `seed` and\nstored as `self.rng`.\nIt is used internally wherever random choices are needed (e.g., surrogate\nselection probabilities).\n\n`set_seed()` then calls `random.seed(self.seed)` and `np.random.seed(self.seed)`\nto seed Python's global and NumPy's global generators, providing a\nreproducibility guarantee across the full call stack.\n\n::: {#a97bf492 .cell execution_count=9}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)], seed=42)\nprint(f\"rng type: {type(opt.rng).__name__}\")\n# Draw a sample — should be deterministic across invocations\nsample = opt.rng.uniform()\nprint(f\"First rng draw: {sample:.6f}\")\n\nopt2 = SpotOptim(fun=sphere, bounds=[(-5, 5)], seed=42)\nsample2 = opt2.rng.uniform()\nassert sample == sample2, \"RNG not reproducible with same seed\"\nprint(\"RNG reproducibility check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nrng type: RandomState\nFirst rng draw: 0.374540\nRNG reproducibility check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 10 — Factor Maps and Bounds Pre-processing\n\n```python\nself._factor_maps = {}\nself._original_bounds = self.bounds.copy()\nself.process_factor_bounds()\n```\n\n`self._factor_maps` is a dict mapping each dimension index to an\n`{int: str}` lookup table.\nIt is empty for purely numeric problems and populated by `process_factor_bounds()`\nwhen any bound is specified as a tuple of strings, e.g., `(\"red\", \"green\", \"blue\")`.\n\n`self._original_bounds` preserves the user-supplied bounds before any\ninteger-encoding transformation.\n\n`process_factor_bounds()` converts string-level bounds to their integer\nequivalents so the optimizer works in a uniform numeric space:\n\n```python\n# before: bounds = [(\"red\", \"green\", \"blue\"), (-5.0, 5.0)]\n# after: bounds = [(0, 2), (-5.0, 5.0)]\n# factor_maps = {0: {0: \"red\", 1: \"green\", 2: \"blue\"}}\n```\n\n::: {#8ba8da66 .cell execution_count=10}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\n# Numeric problem — factor_maps stays empty\nopt_num = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)])\nassert opt_num._factor_maps == {}\nprint(\"No factors: _factor_maps is empty.\")\n\n# Categorical dimension\nimport numpy as np\ndef cat_fun(X):\n X = np.atleast_2d(X)\n return X[:, 1].astype(float) ** 2\n\nopt_cat = SpotOptim(fun=cat_fun, bounds=[(\"red\", \"green\", \"blue\"), (-5.0, 5.0)])\nassert 0 in opt_cat._factor_maps\nassert opt_cat._factor_maps[0] == {0: \"red\", 1: \"green\", 2: \"blue\"}\nassert opt_cat.bounds[0] == (0, 2)\nprint(f\"factor_maps: {opt_cat._factor_maps}\")\nprint(\"Factor bounds check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nNo factors: _factor_maps is empty.\nfactor_maps: {0: {0: 'red', 1: 'green', 2: 'blue'}}\nFactor bounds check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 11 — Dimension Count (`n_dim`)\n\n```python\nself.n_dim = len(self.bounds)\n```\n\nAfter `process_factor_bounds()` has resolved all string-level dimensions,\n`n_dim` is calculated as the number of remaining bounds.\nFor problems with dimension reduction (fixed bounds, see Step 15),\n`n_dim` will later reflect the reduced dimension count.\n\n::: {#81245f07 .cell execution_count=11}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5), (-5, 5)])\nprint(f\"n_dim: {opt.n_dim}\")\nassert opt.n_dim == 3\nprint(\"n_dim check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nn_dim: 3\nn_dim check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 12 — Variable Type Detection (`detect_var_type`)\n\n```python\nif self.var_type is None:\n self.var_type = self.detect_var_type()\n```\n\n`detect_var_type()` infers the variable type for each dimension from the bounds.\nFor dimensions with purely numeric bounds it returns `\"float\"` — it does not\nauto-promote integer-looking bounds like `(0, 5)` to `\"int\"`.\nFactor dimensions created in Step 10 (string-tuple bounds) are already typed\nas `\"factor\"` and are left untouched.\n\nTo use integer variables you must pass `var_type=[\"int\", ...]` explicitly.\n\n::: {#f0c9a7a8 .cell execution_count=12}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nimport numpy as np\n\n# Numeric bounds always yield float\nopt_f = SpotOptim(fun=sphere, bounds=[(-5.0, 5.0), (-5.0, 5.0)])\nprint(f\"var_type (floats) : {opt_f.var_type}\")\nassert all(t == \"float\" for t in opt_f.var_type)\n\n# Integer-looking bounds still yield float unless var_type is explicit\nopt_auto = SpotOptim(fun=sphere, bounds=[(0, 5), (0, 5)])\nprint(f\"var_type (no explicit) : {opt_auto.var_type}\")\nassert all(t == \"float\" for t in opt_auto.var_type)\n\n# Explicitly requesting int variables\nopt_int = SpotOptim(fun=sphere, bounds=[(0, 5), (0, 5)], var_type=[\"int\", \"int\"])\nprint(f\"var_type (explicit int) : {opt_int.var_type}\")\nassert all(t == \"int\" for t in opt_int.var_type)\n\n# Factor dim from string-tuple bounds\ndef cat_fun(X):\n X = np.atleast_2d(X)\n return X[:, 1].astype(float) ** 2\n\nopt_cat = SpotOptim(fun=cat_fun, bounds=[(\"a\", \"b\", \"c\"), (-5.0, 5.0)])\nprint(f\"var_type (factor + float) : {opt_cat.var_type}\")\nassert opt_cat.var_type[0] == \"factor\"\nassert opt_cat.var_type[1] == \"float\"\nprint(\"detect_var_type check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nvar_type (floats) : ['float', 'float']\nvar_type (no explicit) : ['float', 'float']\nvar_type (explicit int) : ['int', 'int']\nvar_type (factor + float) : ['factor', 'float']\ndetect_var_type check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 13 — Bound Modification (`modify_bounds_based_on_var_type`)\n\n```python\nself.modify_bounds_based_on_var_type()\n```\n\n`modify_bounds_based_on_var_type()` adjusts the bounds to be consistent with\nthe declared variable types.\nFor `\"int\"` and `\"factor\"` variables it ensures bounds are expressed as integers.\nFor `\"float\"` variables it converts bounds to `float`.\nThis makes downstream arithmetic type-safe and avoids mixed int/float arrays.\n\n::: {#ff7ee58d .cell execution_count=13}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(0, 5), (-3.0, 3.0)], var_type=[\"int\", \"float\"])\nlower_types = [type(b[0]) for b in opt.bounds]\nprint(f\"Lower bound types: {lower_types}\")\n# int dimension lower bound should be int, float dimension should be float\nassert isinstance(opt.bounds[0][0], int)\nassert isinstance(opt.bounds[1][0], float)\nprint(\"modify_bounds_based_on_var_type check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nLower bound types: [, ]\nmodify_bounds_based_on_var_type check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 14 — Numpy Bound Arrays (`lower`, `upper`)\n\n```python\nself.lower = np.array([b[0] for b in self.bounds])\nself.upper = np.array([b[1] for b in self.bounds])\n```\n\nThe list-of-tuples `self.bounds` is unpacked into two numpy arrays:\n\n- `self.lower` — 1-D array of lower bounds, one entry per dimension\n- `self.upper` — 1-D array of upper bounds, one entry per dimension\n\nThese arrays are used throughout the class for vectorized bound arithmetic,\nfor example, when scaling Latin Hypercube samples — `lower + X_unit * (upper - lower)`.\n\n::: {#ac0650d5 .cell execution_count=14}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (0, 10)])\nprint(f\"lower: {opt.lower}\")\nprint(f\"upper: {opt.upper}\")\nassert np.array_equal(opt.lower, [-5, 0])\nassert np.array_equal(opt.upper, [5, 10])\nprint(\"lower/upper arrays check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nlower: [-5. 0.]\nupper: [ 5. 10.]\nlower/upper arrays check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 15 — Default Variable Names\n\n```python\nif self.var_name is None:\n self.var_name = [f\"x{i}\" for i in range(self.n_dim)]\n```\n\nIf neither the constructor argument nor the function attribute provided names,\neach dimension receives an auto-generated name `\"x0\"`, `\"x1\"`, etc.\nNames appear in console output, error messages, and plot axis labels.\n\n::: {#e4fc05b5 .cell execution_count=15}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt_auto = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)])\nprint(f\"var_name (auto): {opt_auto.var_name}\")\nassert opt_auto.var_name == [\"x0\", \"x1\"]\n\nopt_named = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], var_name=[\"lr\", \"wd\"])\nprint(f\"var_name (user): {opt_named.var_name}\")\nassert opt_named.var_name == [\"lr\", \"wd\"]\nprint(\"var_name check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nvar_name (auto): ['x0', 'x1']\nvar_name (user): ['lr', 'wd']\nvar_name check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 16 — Default Transformation Normalization (`handle_default_var_trans`)\n\n```python\nself.handle_default_var_trans()\n```\n\n`handle_default_var_trans()` normalizes the `var_trans` list without\napplying any transformation.\nIt replaces the strings `\"id\"` and `\"None\"` and Python `None` values with\n`None` so that all downstream code can use a single `None` check.\nIf `var_trans` was not provided, it initializes a list of `None` values\nwith length `n_dim`.\nIt also validates that the list length matches `n_dim`.\n\n::: {#a02c68b7 .cell execution_count=16}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt_none = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)])\nprint(f\"var_trans (default): {opt_none.var_trans}\")\nassert opt_none.var_trans == [None, None]\n\nopt_id = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], var_trans=[\"id\", \"None\"])\nprint(f\"var_trans (id/None): {opt_id.var_trans}\")\nassert all(t is None for t in opt_id.var_trans)\n\nopt_log = SpotOptim(fun=sphere, bounds=[(1, 100), (1, 100)], var_trans=[\"log10\", None])\nprint(f\"var_trans (log10): {opt_log.var_trans}\")\nassert opt_log.var_trans[0] == \"log10\"\nassert opt_log.var_trans[1] is None\nprint(\"handle_default_var_trans check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nvar_trans (default): [None, None]\nvar_trans (id/None): [None, None]\nvar_trans (log10): ['log10', None]\nhandle_default_var_trans check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 17 — Bound Snapshots and Transformation (`transform_bounds`)\n\n```python\nself._original_lower = self.lower.copy()\nself._original_upper = self.upper.copy()\nself.transform_bounds()\n```\n\nBefore any transformation is applied, `_original_lower` and `_original_upper`\nare created as copies of `self.lower` and `self.upper`.\nThese snapshots preserve the natural-space bounds and are used:\n\n- In `validate_x0()` to confirm that a starting point is within the original domain.\n- In reporting and visualization to display the problem in human-readable units.\n\n`transform_bounds()` then replaces `self.lower`, `self.upper`, and `self.bounds`\nwith the values in transformed space.\nFor example, with `var_trans=[\"log10\"]` and `bounds=[(1, 100)]`:\n\n- `_original_lower = [1]`, `_original_upper = [100]`\n- After transformation: `lower = [0.0]`, `upper = [2.0]`\n\n`transform_bounds()` also handles reversed bounds that arise from monotone\ndecreasing transforms such as `reciprocal`.\n\n::: {#471421d5 .cell execution_count=17}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(1, 100), (1, 100)],\n var_trans=[\"log10\", \"log10\"],\n)\nprint(f\"_original_lower : {opt._original_lower}\")\nprint(f\"_original_upper : {opt._original_upper}\")\nprint(f\"lower (transformed) : {opt.lower}\")\nprint(f\"upper (transformed) : {opt.upper}\")\n\nassert np.isclose(opt._original_lower[0], 1.0)\nassert np.isclose(opt._original_upper[0], 100.0)\nassert np.isclose(opt.lower[0], np.log10(1)) # 0.0\nassert np.isclose(opt.upper[0], np.log10(100)) # 2.0\nprint(\"transform_bounds check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n_original_lower : [1. 1.]\n_original_upper : [100. 100.]\nlower (transformed) : [0. 0.]\nupper (transformed) : [2. 2.]\ntransform_bounds check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 18 — Dimension Reduction (`setup_dimension_reduction`)\n\n```python\nself.setup_dimension_reduction()\n```\n\n`setup_dimension_reduction()` identifies dimensions where `lower == upper`\n(fixed variables that carry no information for the optimizer).\nIt stores a boolean mask `self.ident` where `True` marks fixed dimensions.\n\nTwo sets of attributes coexist after this step:\n\n- `all_*` attributes (`all_lower`, `all_upper`, `all_var_name`, etc.) hold the\n full-dimensional representation including fixed dimensions.\n- The main attributes (`self.lower`, `self.upper`, `self.bounds`, `self.n_dim`)\n are reduced to the active (non-fixed) dimensions only.\n\nThe flag `self.red_dim` is `True` when at least one dimension was removed.\n\n::: {#2c9606c5 .cell execution_count=18}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef fun3d(X):\n X = np.atleast_2d(X)\n return np.sum(X**2, axis=1)\n\n# dim 1 is fixed at 5.0\nopt = SpotOptim(fun=fun3d, bounds=[(-5, 5), (5, 5), (-5, 5)])\nprint(f\"red_dim : {opt.red_dim}\")\nprint(f\"ident mask : {opt.ident}\")\nprint(f\"n_dim (reduced) : {opt.n_dim}\")\nprint(f\"all_lower (full) : {opt.all_lower}\")\n\nassert opt.red_dim == True # numpy.bool_ — use == not `is`\nassert opt.ident[1] == True # numpy.bool_ — use == not `is`\nassert opt.n_dim == 2 # only 2 free dimensions\nassert len(opt.all_lower) == 3 # full-dim snapshot preserved\nprint(\"setup_dimension_reduction check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nred_dim : True\nident mask : [False True False]\nn_dim (reduced) : 2\nall_lower (full) : [-5. 5. -5.]\nsetup_dimension_reduction check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 19 — Starting Point Validation (`validate_x0`)\n\n```python\nif self.x0 is not None:\n self.x0 = self.validate_x0(self.x0)\n```\n\nWhen the user supplies `x0`, it is validated and transformed to internal scale\nbefore any optimization begins.\n`validate_x0()` performs the following checks:\n\n1. `x0` must be 1-D or 2-D (a scalar or 3-D array raises `ValueError`).\n2. The length of `x0` must match the full dimension count (before reduction).\n3. Each component must lie within its natural-space bounds.\n4. Fixed dimensions must exactly equal their fixed value.\n\nIf all checks pass, `validate_x0()` applies `transform_X()` to convert `x0`\nto the same internal (transformed and reduced) representation used during\noptimization.\n\n::: {#f11750a4 .cell execution_count=19}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\n# x0 in natural scale, dim 1 is fixed\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (5, 5), (-5, 5)],\n x0=np.array([1.0, 5.0, 3.0]),\n)\nprint(f\"x0 (internal scale): {opt.x0}\")\n# After reduction, x0 should only contain the 2 free dims\nassert opt.x0.shape == (2,)\nprint(\"x0 = internal scale shape:\", opt.x0.shape)\n\n# x0 outside bounds must raise\ntry:\n SpotOptim(fun=sphere, bounds=[(-5, 5)], x0=np.array([10.0]))\n raise AssertionError(\"Should have raised ValueError\")\nexcept ValueError as e:\n print(f\"Caught: {e}\")\nprint(\"validate_x0 check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nx0 (internal scale): [1. 3.]\nx0 = internal scale shape: (2,)\nCaught: x0 (x0) = 10.0 is outside bounds [-5.0, 5.0]. \nvalidate_x0 check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 20 — Surrogate Initialization (`init_surrogate`)\n\n```python\nself.init_surrogate()\n```\n\n`init_surrogate()` sets up the surrogate model used to approximate the\nobjective function between evaluations.\nThree scenarios are handled:\n\n- `surrogate=None` (default): A `GaussianProcessRegressor` with a\n `ConstantKernel * Matern(nu=2.5)` kernel, 100 optimizer restarts,\n and `normalize_y=True` is created automatically.\n- `surrogate=[list]`: A multi-surrogate setup is configured.\n Probability weights (`self._prob_surrogate`) and per-surrogate\n `_max_surrogate_points_list` are computed.\n- `surrogate=`: The provided object is used as-is.\n\nAfter `init_surrogate()`, the following attributes are set:\n\n- `self.surrogate` — the active surrogate model\n- `self._surrogates_list` — list or `None`\n- `self._prob_surrogate` — selection probabilities or `None`\n- `self._max_surrogate_points_list` — per-surrogate max point limits\n- `self._active_max_surrogate_points` — current active limit\n\n::: {#a494ea4c .cell execution_count=20}\n``` {.python .cell-code}\nfrom sklearn.gaussian_process import GaussianProcessRegressor\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\n# Default surrogate\nopt_default = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nprint(f\"default surrogate type: {type(opt_default.surrogate).__name__}\")\nassert isinstance(opt_default.surrogate, GaussianProcessRegressor)\n\n# User-supplied surrogate\ncustom_gp = GaussianProcessRegressor(n_restarts_optimizer=5)\nopt_custom = SpotOptim(fun=sphere, bounds=[(-5, 5)], surrogate=custom_gp)\nassert opt_custom.surrogate is custom_gp\nprint(\"init_surrogate check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ndefault surrogate type: GaussianProcessRegressor\ninit_surrogate check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 21 — Latin Hypercube Sampler (`lhs_sampler`)\n\n```python\nself.lhs_sampler = LatinHypercube(d=self.n_dim, rng=self.seed)\n```\n\nA `scipy.stats.qmc.LatinHypercube` sampler is created for generating\nspace-filling initial designs.\n`d=self.n_dim` sets the dimensionality to the reduced dimension count.\n`rng=self.seed` seeds the sampler for reproducibility.\n\nThe primary parameter `rng=` is used instead of the legacy alias `seed=`\nto align with the current scipy API.\nThe sampler is called in `generate_initial_design()` via\n`self.lhs_sampler.random(n=self.n_initial)`, which returns points in `[0, 1]^d`\nthat are then scaled to `[lower, upper]`.\n\n::: {#85a25f1a .cell execution_count=21}\n``` {.python .cell-code}\nfrom scipy.stats.qmc import LatinHypercube\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\nimport numpy as np\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], seed=42)\nprint(f\"lhs_sampler type: {type(opt.lhs_sampler).__name__}\")\nassert isinstance(opt.lhs_sampler, LatinHypercube)\n\n# Reproducibility: two samplers with rng=42 produce identical draws\ns1 = LatinHypercube(d=2, rng=42).random(5)\ns2 = LatinHypercube(d=2, rng=42).random(5)\nassert np.allclose(s1, s2)\nprint(\"lhs_sampler reproducibility check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nlhs_sampler type: LatinHypercube\nlhs_sampler reproducibility check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 22 — Window Size Default\n\n```python\nif self.window_size is None:\n if self.restart_after_n is not None:\n self.window_size = self.restart_after_n\n else:\n self.window_size = 100\n```\n\n`window_size` controls how many of the most recent evaluated points are\nincluded in the surrogate's training window each iteration\n(used together with `selection_method`).\n\nIf the user did not set `window_size` explicitly:\n\n- When `restart_after_n` is set, `window_size` mirrors it so that the\n sliding window aligns with the restart cycle length.\n- Otherwise it falls back to `100`, large enough to include all points\n in a typical short optimization run.\n\n::: {#15a2df6a .cell execution_count=22}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\n# Without restart — defaults to 100\nopt1 = SpotOptim(fun=sphere, bounds=[(-5, 5)])\nprint(f\"window_size (no restart): {opt1.window_size}\")\nassert opt1.window_size == 100\n\n# With restart_after_n — matches it\nopt2 = SpotOptim(fun=sphere, bounds=[(-5, 5)], restart_after_n=40)\nprint(f\"window_size (restart=40): {opt2.window_size}\")\nassert opt2.window_size == 40\n\n# Explicit override\nopt3 = SpotOptim(fun=sphere, bounds=[(-5, 5)], window_size=25)\nprint(f\"window_size (explicit=25): {opt3.window_size}\")\nassert opt3.window_size == 25\nprint(\"window_size check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nwindow_size (no restart): 100\nwindow_size (restart=40): 40\nwindow_size (explicit=25): 25\nwindow_size check passed.\n```\n:::\n:::\n\n\n---\n\n## Step 23 — TensorBoard Setup\n\n```python\nself._clean_tensorboard_logs()\nself._init_tensorboard_writer()\n```\n\nThe final two calls set up optional TensorBoard logging.\n\n`_clean_tensorboard_logs()` deletes existing log files at `tensorboard_path`\nif `tensorboard_clean=True` was requested.\n\n`_init_tensorboard_writer()` creates a `SummaryWriter` if `tensorboard_log=True`,\nwriting event files to `tensorboard_path`.\nWhen logging is disabled (the default), both methods are no-ops so there is\nno runtime cost.\n\n::: {#db245f90 .cell execution_count=23}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\n# Default: TensorBoard disabled — no writer created\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)], tensorboard_log=False)\nhas_writer = hasattr(opt, \"tb_writer\") and opt.tb_writer is not None\nprint(f\"tb_writer active: {has_writer} (expected False)\")\nassert not has_writer\nprint(\"TensorBoard no-op check passed.\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\ntb_writer active: False (expected False)\nTensorBoard no-op check passed.\n```\n:::\n:::\n\n\n---\n\n## Complete Initialization Sequence Summary\n\n@tbl-init summarises every step performed by `__init__()` in order:\n\n| Step | Action | Key attributes set |\n|-----:|--------|-------------------|\n| 1 | `warnings.filterwarnings(warnings_filter)` | — |\n| 2 | Machine epsilon | `self.eps` |\n| 3 | `tolerance_x` default | `tolerance_x` |\n| 4 | Parameter inference from `fun` | `bounds`, `var_type`, `var_name`, `var_trans` |\n| 5 | Validate `max_iter`; default `acquisition_optimizer_kwargs` | — |\n| 6 | Create `SpotOptimConfig` | `self.config` |\n| 7 | Create `SpotOptimState` | `self.state` |\n| 8 | Store callable and `objective_names` | `self.fun`, `self.objective_names` |\n| 9 | Seed RNG | `self.rng` |\n| 10 | Factor maps, original bounds, `process_factor_bounds()` | `self._factor_maps`, `self._original_bounds`, `self.bounds` |\n| 11 | Compute `n_dim` | `self.n_dim` |\n| 12 | `detect_var_type()` | `self.var_type` |\n| 13 | `modify_bounds_based_on_var_type()` | `self.bounds` |\n| 14 | Unpack bounds to arrays | `self.lower`, `self.upper` |\n| 15 | Default variable names | `self.var_name` |\n| 16 | `handle_default_var_trans()` | `self.var_trans` |\n| 17 | Snapshot bounds, `transform_bounds()` | `self._original_lower`, `self._original_upper`, `self.bounds` |\n| 18 | `setup_dimension_reduction()` | `self.ident`, `self.red_dim`, `all_*` attributes |\n| 19 | `validate_x0()` | `self.x0` (transformed, reduced) |\n| 20 | `init_surrogate()` | `self.surrogate`, `self._surrogates_list` |\n| 21 | Create `lhs_sampler` | `self.lhs_sampler` |\n| 22 | `window_size` default | `self.window_size` |\n| 23 | TensorBoard setup | `self.tb_writer` |\n\n: Complete Initialization Sequence Summary {#tbl-init}\n\n", "supporting": [ "spotoptim_init_files/figure-html" ], diff --git a/_freeze/docs/user_guide/core/execute-results/html.json b/_freeze/docs/user_guide/core/execute-results/html.json index a682e258..2d7cb4ca 100644 --- a/_freeze/docs/user_guide/core/execute-results/html.json +++ b/_freeze/docs/user_guide/core/execute-results/html.json @@ -1,8 +1,8 @@ { - "hash": "e18d837095d66ea02c396307d698601b", + "hash": "12dc278a1cbd91482e148db92529d785", "result": { "engine": "jupyter", - "markdown": "---\ntitle: \"Core Infrastructure\"\ndescription: \"Data protocols, storage, and experiment control that underpin spotoptim.\"\n---\n\nThe `core` subpackage provides the foundational infrastructure that\nthe rest of spotoptim builds on. Most users interact with it indirectly\nthrough `SpotOptim`, but understanding these components helps when\nextending the package or building custom workflows.\n\n---\n\n## SpotOptimProtocol\n\n`SpotOptimProtocol` is a structural typing protocol (PEP 544) that\ndefines the interface extracted modules expect. Instead of importing\nthe concrete `SpotOptim` class (which would create circular imports),\nmodules like `optimizer.steady_state` and `reporting.analysis` accept\nany object matching this protocol.\n\nThe protocol declares configuration attributes (`bounds`, `max_iter`,\n`acquisition`, `surrogate`, etc.) and mutable state attributes (`X_`,\n`y_`, `best_x_`, `best_y_`, etc.). This makes it straightforward to\ntest extracted functions with mock objects or to use them outside the\nmain `SpotOptim` class.\n\n```python\n# Signature (not executable — for illustration)\nclass SpotOptimProtocol(Protocol):\n bounds: list\n max_iter: int\n surrogate: object\n acquisition: str\n X_: np.ndarray\n y_: np.ndarray\n ...\n```\n\nSee `core/protocol.py` in the source for the full attribute list.\n\n---\n\n## Storage\n\nThe `core.storage` module manages the optimizer's internal data arrays.\nFunctions like `init_storage()` and `update_storage()` handle appending\nnew evaluation results, updating running statistics (mean, variance,\nbest-so-far), and tracking success rates.\n\nThese are called automatically by `SpotOptim` during optimization. They\naccept a `SpotOptimProtocol` instance rather than the concrete class.\n\n---\n\n## ExperimentControl\n\n`ExperimentControl` is a dataclass for managing PyTorch-based experiment\nconfigurations. It bundles dataset, model class, hyperparameters, device\nsettings, and training parameters into a single object.\n\n::: {#dd717344 .cell execution_count=1}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim.core.data import SpotDataFromArray\nfrom spotoptim.core.experiment import ExperimentControl\nfrom spotoptim.nn.mlp import MLP\n\n# Prepare a minimal dataset\nX = np.array([[0.1, 0.2], [0.3, 0.4]])\ny = np.array([[1.0], [2.0]])\ndataset = SpotDataFromArray(x_train=X, y_train=y)\n\n# Create experiment control\nec = ExperimentControl(\n dataset=dataset,\n model_class=MLP,\n hyperparameters={\"l1\": 16, \"num_hidden_layers\": 1, \"lr\": 1e-3},\n experiment_name=\"demo\",\n seed=0,\n device=\"cpu\",\n batch_size=32,\n epochs=10,\n)\nprint(f\"Experiment: {ec.experiment_name}\")\nprint(f\"Device : {ec.device}\")\nprint(f\"Batch size: {ec.batch_size}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nExperiment: demo\nDevice : cpu\nBatch size: 32\n```\n:::\n:::\n\n\n---\n\n## See Also\n\n- [The SpotOptim Class](spotoptim.qmd) --- The main class that uses these core components\n- [Utilities](utils.qmd) --- Helper functions for boundaries, PCA, OCBA\n\n", + "markdown": "---\ntitle: \"Core Infrastructure\"\ndescription: \"Data protocols, storage, and experiment control that underpin spotoptim.\"\n---\n\nThe `core` subpackage provides the foundational infrastructure that\nthe rest of spotoptim builds on. Most users interact with it indirectly\nthrough `SpotOptim`, but understanding these components helps when\nextending the package or building custom workflows.\n\n---\n\n## SpotOptimProtocol\n\n`SpotOptimProtocol` is a structural typing protocol (PEP 544) that\ndefines the interface extracted modules expect. Instead of importing\nthe concrete `SpotOptim` class (which would create circular imports),\nmodules like `reporting.analysis` accept any object matching this protocol.\n\nThe protocol declares configuration attributes (`bounds`, `max_iter`,\n`acquisition`, `surrogate`, etc.) and mutable state attributes (`X_`,\n`y_`, `best_x_`, `best_y_`, etc.). This makes it straightforward to\ntest extracted functions with mock objects or to use them outside the\nmain `SpotOptim` class.\n\n```python\n# Signature (not executable — for illustration)\nclass SpotOptimProtocol(Protocol):\n bounds: list\n max_iter: int\n surrogate: object\n acquisition: str\n X_: np.ndarray\n y_: np.ndarray\n ...\n```\n\nSee `core/protocol.py` in the source for the full attribute list.\n\n---\n\n## Storage\n\nThe `core.storage` module manages the optimizer's internal data arrays.\nFunctions like `init_storage()` and `update_storage()` handle appending\nnew evaluation results, updating running statistics (mean, variance,\nbest-so-far), and tracking success rates.\n\nThese are called automatically by `SpotOptim` during optimization. They\naccept a `SpotOptimProtocol` instance rather than the concrete class.\n\n---\n\n## ExperimentControl\n\n`ExperimentControl` is a dataclass for managing PyTorch-based experiment\nconfigurations. It bundles dataset, model class, hyperparameters, device\nsettings, and training parameters into a single object.\n\n::: {#8564fc75 .cell execution_count=1}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim.core.data import SpotDataFromArray\nfrom spotoptim.core.experiment import ExperimentControl\nfrom spotoptim.nn.mlp import MLP\n\n# Prepare a minimal dataset\nX = np.array([[0.1, 0.2], [0.3, 0.4]])\ny = np.array([[1.0], [2.0]])\ndataset = SpotDataFromArray(x_train=X, y_train=y)\n\n# Create experiment control\nec = ExperimentControl(\n dataset=dataset,\n model_class=MLP,\n hyperparameters={\"l1\": 16, \"num_hidden_layers\": 1, \"lr\": 1e-3},\n experiment_name=\"demo\",\n seed=0,\n device=\"cpu\",\n batch_size=32,\n epochs=10,\n)\nprint(f\"Experiment: {ec.experiment_name}\")\nprint(f\"Device : {ec.device}\")\nprint(f\"Batch size: {ec.batch_size}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nExperiment: demo\nDevice : cpu\nBatch size: 32\n```\n:::\n:::\n\n\n---\n\n## See Also\n\n- [The SpotOptim Class](spotoptim.qmd) --- The main class that uses these core components\n- [Utilities](utils.qmd) --- Helper functions for boundaries, PCA, OCBA\n\n", "supporting": [ "core_files/figure-html" ], diff --git a/_freeze/docs/user_guide/execute-results/html.json b/_freeze/docs/user_guide/execute-results/html.json index 78edfa5d..fbadfb41 100644 --- a/_freeze/docs/user_guide/execute-results/html.json +++ b/_freeze/docs/user_guide/execute-results/html.json @@ -1,10 +1,10 @@ { - "hash": "5d6834fea9b4c46aec0f59da66c8bd7f", + "hash": "7d6fafc0e710e0fbac1ddedafba21994", "result": { "engine": "jupyter", - "markdown": "---\ntitle: \"User Guide\"\ndescription: \"Comprehensive guide to the spotoptim package: surrogate-model-based optimization of expensive black-box functions.\"\n---\n\nspotoptim is a Python toolbox for Sequential Parameter Optimization (SPO).\nIt optimizes expensive black-box functions by building a surrogate model\n(typically Kriging) from a small initial design, then iteratively selecting\nnew evaluation points using an acquisition function. The result is a\nscipy-compatible `OptimizeResult` object.\n\nThe core loop looks like this:\n\n```\nInitial Design --> Evaluate --> Fit Surrogate --> Optimize Acquisition\n ^ |\n | Infill (new point) |\n +---------------------------------------------------------+\n```\n\n---\n\n## Quick Start\n\n::: {#4263f8c7 .cell execution_count=1}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=20,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Best x : {result.x}\")\nprint(f\"Best f(x) : {result.fun:.6f}\")\nprint(f\"Evaluations: {result.nfev}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nBest x : [-0.00016718 0.00071419]\nBest f(x) : 0.000001\nEvaluations: 20\n```\n:::\n:::\n\n\n---\n\n## How to Read This Guide\n\nEach page below covers one spotoptim module. Start with\n[The SpotOptim Class](user_guide/spotoptim.qmd) for the core workflow,\nthen explore the modules that match your use case.\n\n---\n\n## Module Index\n\n### Core Workflow\n\n- [The SpotOptim Class](user_guide/spotoptim.qmd) --- Central optimizer: constructor, `optimize()`, `OptimizeResult`, variable types, noisy optimization\n- [Acquisition and Infill](user_guide/optimizer.qmd) --- Acquisition functions (`\"y\"`, `\"ei\"`, `\"pi\"`), infill strategies, acquisition optimizers\n- [Surrogate Models](user_guide/surrogate.qmd) --- Kriging, SimpleKriging, MLPSurrogate, custom sklearn surrogates\n\n### Functions and Designs\n\n- [Built-in Test Functions](user_guide/function.qmd) --- Single- and multi-objective benchmark functions, custom objectives\n- [Sampling and Designs](user_guide/sampling.qmd) --- Latin Hypercube, Sobol, grid, and clustered experimental designs\n\n### Analysis and Visualization\n\n- [Visualization](user_guide/plot.qmd) --- Optimization progress, surrogate surfaces, contour plots\n- [Reporting and Analysis](user_guide/reporting.qmd) --- Results tables, variable importance, sensitivity analysis\n- [Model Inspection](user_guide/inspection.qmd) --- Feature importance (MDI, permutation), actual-vs-predicted plots\n\n### Utilities\n\n- [Utilities](user_guide/utils.qmd) --- Boundaries, transforms, PCA, OCBA, parallel helpers\n- [Neural Networks](user_guide/nn.qmd) --- PyTorch MLP and LinearRegressor for building objectives\n- [Datasets](user_guide/data.qmd) --- DiabetesDataset and data loaders for PyTorch workflows\n\n### Advanced Topics\n\n- [Multi-Objective Optimization](user_guide/mo.qmd) --- Pareto fronts, desirability functions, ZDT/DTLZ benchmarks\n- [Core Infrastructure](user_guide/core.qmd) --- SpotOptimProtocol, storage, experiment control\n- [Hyperparameter Definition](user_guide/hyperparameters.qmd) --- ParameterSet fluent API for defining search spaces\n- [Factor Analysis](user_guide/factor_analyzer.qmd) --- Exploratory and confirmatory factor analysis\n- [Exploratory Data Analysis](user_guide/eda.qmd) --- Histograms and boxplots for optimization data\n- [Triangulation Candidates](user_guide/tricands.qmd) --- Delaunay-based candidate point generation\n\n---\n\n## Module Map\n\n```\nsrc/spotoptim/\n├── SpotOptim.py # Core optimizer\n├── core/ # Protocol, storage, experiment control\n├── optimizer/ # Acquisition, steady-state, scipy wrapper\n├── surrogate/ # Kriging, MLP surrogate, Nystroem, sklearn pipeline\n├── nn/ # PyTorch MLP, LinearRegressor\n├── function/ # Objective functions (single/multi-objective, remote, torch)\n├── sampling/ # LHS, Sobol, grid, clustered designs\n├── reporting/ # Results extraction, analysis utilities\n├── plot/ # Surrogate visualization, contour, multi-objective\n├── utils/ # Boundaries, transforms, PCA, OCBA, TensorBoard, parallel\n├── mo/ # Multi-objective: Morris-Mitchell, Pareto front\n├── hyperparameters/ # Parameter set management for NN tuning\n├── data/ # Dataset loaders (e.g., DiabetesDataset)\n├── inspection/ # Model/surrogate inspection (importance, predictions)\n├── factor_analyzer/ # Factor analysis\n├── eda/ # Exploratory data analysis\n└── tricands/ # Triangulation-based candidate generation\n```\n\n---\n\n## See Also\n\n- [Getting Started](sequential-parameter-optimization-cookbook.qmd) --- External cookbook with detailed tutorials\n- [Examples](examples.qmd) --- Quick runnable examples\n- [API Reference](reference/index.qmd) --- Complete auto-generated API documentation\n\n", + "markdown": "---\ntitle: \"User Guide\"\ndescription: \"Comprehensive guide to the spotoptim package: surrogate-model-based optimization of expensive black-box functions.\"\n---\n\nspotoptim is a Python toolbox for Sequential Parameter Optimization (SPO).\nIt optimizes expensive black-box functions by building a surrogate model\n(typically Kriging) from a small initial design, then iteratively selecting\nnew evaluation points using an acquisition function. The result is a\nscipy-compatible `OptimizeResult` object.\n\nThe core loop looks like this:\n\n```\nInitial Design --> Evaluate --> Fit Surrogate --> Optimize Acquisition\n ^ |\n | Infill (new point) |\n +---------------------------------------------------------+\n```\n\n---\n\n## Quick Start\n\n::: {#329530c1 .cell execution_count=1}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=20,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Best x : {result.x}\")\nprint(f\"Best f(x) : {result.fun:.6f}\")\nprint(f\"Evaluations: {result.nfev}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nBest x : [-0.00016718 0.00071419]\nBest f(x) : 0.000001\nEvaluations: 20\n```\n:::\n:::\n\n\n---\n\n## How to Read This Guide\n\nEach page below covers one spotoptim module. Start with\n[The SpotOptim Class](user_guide/spotoptim.qmd) for the core workflow,\nthen explore the modules that match your use case.\n\n---\n\n## Module Index\n\n### Core Workflow\n\n- [The SpotOptim Class](user_guide/spotoptim.qmd) --- Central optimizer: constructor, `optimize()`, `OptimizeResult`, variable types, noisy optimization\n- [Acquisition and Infill](user_guide/optimizer.qmd) --- Acquisition functions (`\"y\"`, `\"ei\"`, `\"pi\"`), infill strategies, acquisition optimizers\n- [Surrogate Models](user_guide/surrogate.qmd) --- Kriging, SimpleKriging, MLPSurrogate, custom sklearn surrogates\n\n### Functions and Designs\n\n- [Built-in Test Functions](user_guide/function.qmd) --- Single- and multi-objective benchmark functions, custom objectives\n- [Sampling and Designs](user_guide/sampling.qmd) --- Latin Hypercube, Sobol, grid, and clustered experimental designs\n\n### Analysis and Visualization\n\n- [Visualization](user_guide/plot.qmd) --- Optimization progress, surrogate surfaces, contour plots\n- [Reporting and Analysis](user_guide/reporting.qmd) --- Results tables, variable importance, sensitivity analysis\n- [Model Inspection](user_guide/inspection.qmd) --- Feature importance (MDI, permutation), actual-vs-predicted plots\n\n### Utilities\n\n- [Utilities](user_guide/utils.qmd) --- Boundaries, transforms, PCA, OCBA, TensorBoard logging\n- [Neural Networks](user_guide/nn.qmd) --- PyTorch MLP and LinearRegressor for building objectives\n- [Datasets](user_guide/data.qmd) --- DiabetesDataset and data loaders for PyTorch workflows\n\n### Advanced Topics\n\n- [Multi-Objective Optimization](user_guide/mo.qmd) --- Pareto fronts, desirability functions, ZDT/DTLZ benchmarks\n- [Core Infrastructure](user_guide/core.qmd) --- SpotOptimProtocol, storage, experiment control\n- [Hyperparameter Definition](user_guide/hyperparameters.qmd) --- ParameterSet fluent API for defining search spaces\n- [Factor Analysis](user_guide/factor_analyzer.qmd) --- Exploratory and confirmatory factor analysis\n- [Exploratory Data Analysis](user_guide/eda.qmd) --- Histograms and boxplots for optimization data\n- [Triangulation Candidates](user_guide/tricands.qmd) --- Delaunay-based candidate point generation\n\n---\n\n## Module Map\n\n```\nsrc/spotoptim/\n├── SpotOptim.py # Core optimizer\n├── core/ # Protocol, storage, experiment control\n├── optimizer/ # Acquisition, scipy wrapper\n├── surrogate/ # Kriging, MLP surrogate, Nystroem, sklearn pipeline\n├── nn/ # PyTorch MLP, LinearRegressor\n├── function/ # Objective functions (single/multi-objective, remote, torch)\n├── sampling/ # LHS, Sobol, grid, clustered designs\n├── reporting/ # Results extraction, analysis utilities\n├── plot/ # Surrogate visualization, contour, multi-objective\n├── utils/ # Boundaries, transforms, PCA, OCBA, TensorBoard\n├── mo/ # Multi-objective: Morris-Mitchell, Pareto front\n├── hyperparameters/ # Parameter set management for NN tuning\n├── data/ # Dataset loaders (e.g., DiabetesDataset)\n├── inspection/ # Model/surrogate inspection (importance, predictions)\n├── factor_analyzer/ # Factor analysis\n├── eda/ # Exploratory data analysis\n└── tricands/ # Triangulation-based candidate generation\n```\n\n---\n\n## See Also\n\n- [Getting Started](sequential-parameter-optimization-cookbook.qmd) --- External cookbook with detailed tutorials\n- [Examples](examples.qmd) --- Quick runnable examples\n- [API Reference](reference/index.qmd) --- Complete auto-generated API documentation\n\n", "supporting": [ - "user_guide_files" + "user_guide_files/figure-html" ], "filters": [], "includes": {} diff --git a/_freeze/docs/user_guide/spotoptim/execute-results/html.json b/_freeze/docs/user_guide/spotoptim/execute-results/html.json index f0aea530..bc737665 100644 --- a/_freeze/docs/user_guide/spotoptim/execute-results/html.json +++ b/_freeze/docs/user_guide/spotoptim/execute-results/html.json @@ -1,10 +1,10 @@ { - "hash": "0550ebf4046c08a0db24c669b1cbb356", + "hash": "85696d5bbf2c64801652dd1c28f7c58e", "result": { "engine": "jupyter", - "markdown": "---\ntitle: \"The SpotOptim Class\"\ndescription: \"Central orchestrator for surrogate-model-based optimization: constructor, optimize(), and result inspection.\"\n---\n\n`SpotOptim` is the main entry point for all optimization in spotoptim.\nYou create an instance with your objective function and bounds, call\n`optimize()`, and get back a scipy-compatible `OptimizeResult`.\n\n---\n\n## Minimal Example\n\n::: {#70807ab0 .cell execution_count=1}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=20,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Best x : {result.x}\")\nprint(f\"Best f(x) : {result.fun:.6f}\")\nprint(f\"Evaluations: {result.nfev}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nBest x : [-0.00016718 0.00071419]\nBest f(x) : 0.000001\nEvaluations: 20\n```\n:::\n:::\n\n\nThree ingredients: a callable `fun`, a list of `bounds`, and a budget via\n`max_iter` (total evaluations including the initial design). The optimizer\nbuilds a Kriging surrogate by default and uses predicted-value acquisition\n(`acquisition=\"y\"`).\n\n---\n\n## Key Constructor Parameters\n\nThe constructor accepts many parameters. The most commonly used are:\n\n| Parameter | Default | Description |\n|---|---|---|\n| `fun` | (required) | Objective function: accepts `(n, d)` array, returns `(n,)` array |\n| `bounds` | `None` | List of `(lower, upper)` tuples, one per dimension |\n| `max_iter` | `20` | Total evaluation budget (initial + sequential) |\n| `n_initial` | `10` | Number of initial design points |\n| `surrogate` | `None` | Surrogate model (default: `Kriging(method=\"regression\")`) |\n| `acquisition` | `\"y\"` | Acquisition function: `\"y\"`, `\"ei\"`, or `\"pi\"` |\n| `var_type` | `None` | Variable types: list of `\"float\"`, `\"int\"`, `\"factor\"` |\n| `seed` | `None` | Random seed for reproducibility |\n\nSee the [SpotOptim API](../reference/SpotOptim.SpotOptim.qmd) for the full parameter list.\n\n---\n\n## The OptimizeResult\n\n`optimize()` returns a `scipy.optimize.OptimizeResult` with these fields:\n\n| Field | Type | Description |\n|---|---|---|\n| `x` | `ndarray` | Best point found (original scale) |\n| `fun` | `float` | Best objective value |\n| `nfev` | `int` | Total function evaluations |\n| `nit` | `int` | Sequential iterations (after initial design) |\n| `success` | `bool` | Whether optimization succeeded |\n| `message` | `str` | Termination reason with statistics |\n| `X` | `ndarray` | All evaluated points |\n| `y` | `ndarray` | All objective values |\n\n::: {#1162ae4e .cell execution_count=2}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import ackley\n\nopt = SpotOptim(\n fun=ackley,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=25,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Success : {result.success}\")\nprint(f\"Best x : {result.x}\")\nprint(f\"Best f(x) : {result.fun:.6f}\")\nprint(f\"Iterations: {result.nit}\")\nprint(f\"All points: {result.X.shape}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSuccess : True\nBest x : [ 0.00174499 -0.00130203]\nBest f(x) : 0.006284\nIterations: 15\nAll points: (25, 2)\n```\n:::\n:::\n\n\n---\n\n## Variable Types\n\nspotoptim supports three variable types via the `var_type` parameter:\n\n- `\"float\"` --- continuous real-valued variables (default)\n- `\"int\"` --- integer-constrained variables (rounded after surrogate prediction)\n- `\"factor\"` --- categorical/unordered variables (encoded internally)\n\n::: {#d903272a .cell execution_count=3}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef mixed_objective(X):\n X = np.atleast_2d(X)\n continuous = X[:, 0]\n integer_val = X[:, 1]\n factor_val = X[:, 2]\n return continuous**2 + (integer_val - 3)**2 + factor_val\n\nopt = SpotOptim(\n fun=mixed_objective,\n bounds=[(-5.0, 5.0), (0, 10), (0, 4)],\n var_type=[\"float\", \"int\", \"factor\"],\n var_name=[\"x_cont\", \"x_int\", \"x_cat\"],\n max_iter=25,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Best x : {result.x}\")\nprint(f\"Best f(x) : {result.fun:.6f}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nBest x : [-9.98879117e-04 3.00000000e+00 0.00000000e+00]\nBest f(x) : 0.000001\n```\n:::\n:::\n\n\nWhen `var_type` is not provided, all variables default to `\"float\"`.\n\n---\n\n## Variable Transformations\n\nUse `var_trans` to apply log-transformations to parameters that span\nseveral orders of magnitude (e.g., learning rates):\n\n::: {#52be9c52 .cell execution_count=4}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef obj_with_lr(X):\n X = np.atleast_2d(X)\n lr = X[:, 0]\n weight = X[:, 1]\n return (lr - 0.001)**2 + (weight - 0.5)**2\n\nopt = SpotOptim(\n fun=obj_with_lr,\n bounds=[(1e-5, 1e-1), (0.0, 1.0)],\n var_type=[\"float\", \"float\"],\n var_name=[\"lr\", \"weight\"],\n var_trans=[\"log10\", None],\n max_iter=20,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Best lr : {result.x[0]:.6f}\")\nprint(f\"Best weight : {result.x[1]:.4f}\")\nprint(f\"Best f(x) : {result.fun:.8f}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nBest lr : 0.000010\nBest weight : 0.4981\nBest f(x) : 0.00000477\n```\n:::\n:::\n\n\nWith `var_trans=[\"log10\", None]`, the first variable is optimized in\nlog10 space internally. The bounds are specified in natural (original)\nscale --- here `1e-5` to `1e-1` for the learning rate.\n\n---\n\n## Choosing an Acquisition Function\n\nThe `acquisition` parameter controls how the optimizer selects the next\nevaluation point:\n\n- `\"y\"` (default) --- minimize the surrogate's predicted value (pure exploitation)\n- `\"ei\"` --- Expected Improvement: balances exploitation and exploration\n- `\"pi\"` --- Probability of Improvement: probability of beating the current best\n\n::: {#2456c307 .cell execution_count=5}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import rosenbrock\n\nopt = SpotOptim(\n fun=rosenbrock,\n bounds=[(-2, 2), (-2, 2)],\n acquisition=\"ei\",\n max_iter=25,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Best x : {result.x}\")\nprint(f\"Best f(x) : {result.fun:.6f}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nBest x : [1.24915117 1.47082529]\nBest f(x) : 0.864057\n```\n:::\n:::\n\n\nSee [Acquisition and Infill](optimizer.qmd) for details on acquisition\noptimizers and infill strategies.\n\n---\n\n## Choosing a Surrogate Model\n\nThe default surrogate is `Kriging(method=\"regression\")`. You can pass\nany model that implements `fit(X, y)` and `predict(X, return_std=False)`:\n\n::: {#ec9f40c8 .cell execution_count=6}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.surrogate import Kriging\nfrom spotoptim.function import sphere\n\nkriging = Kriging(method=\"interpolation\", noise=1e-3, seed=0)\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n surrogate=kriging,\n max_iter=20,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Best f(x) : {result.fun:.6f}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nBest f(x) : 0.006474\n```\n:::\n:::\n\n\nSee [Surrogate Models](surrogate.qmd) for Kriging options, MLPSurrogate,\nand how to plug in sklearn estimators.\n\n---\n\n## Noisy Optimization\n\nFor noisy objective functions, use `repeats_initial` and\n`repeats_surrogate` to re-evaluate each design point multiple times.\nAdd `ocba_delta` to enable Optimal Computing Budget Allocation, which\nintelligently allocates extra evaluations to the most promising points.\n\n::: {#6ac7801a .cell execution_count=7}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import noisy_sphere\n\nnp.random.seed(0)\n\nopt = SpotOptim(\n fun=noisy_sphere,\n bounds=[(-5, 5), (-5, 5)],\n repeats_initial=3,\n repeats_surrogate=2,\n ocba_delta=3,\n max_iter=30,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Best x : {result.x}\")\nprint(f\"Best f(x) : {result.fun:.6f}\")\nprint(f\"Evaluations: {result.nfev}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nBest x : [-0.44355715 -0.46646793]\nBest f(x) : 0.395617\nEvaluations: 30\n```\n:::\n:::\n\n\nThe surrogate is fitted on the mean values across repeats, reducing\nthe effect of noise. See [Utilities](utils.qmd) for more on OCBA.\n\n---\n\n## Time Budget\n\nUse `max_time` (in minutes) to set a wall-clock time limit instead of\nor in addition to `max_iter`:\n\n::: {#eef8a708 .cell execution_count=8}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n max_time=0.05,\n max_iter=1000,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Evaluations: {result.nfev}\")\nprint(f\"Best f(x) : {result.fun:.6f}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nEvaluations: 16\nBest f(x) : 0.000001\n```\n:::\n:::\n\n\nThe optimizer stops when either `max_iter` or `max_time` is reached,\nwhichever comes first.\n\n---\n\n## Restarts\n\nWhen the optimizer gets stuck in a local minimum, automatic restarts\ncan help. Set `restart_after_n` to trigger a restart after that many\nconsecutive iterations without improvement:\n\n::: {#929f6118 .cell execution_count=9}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import ackley\n\nopt = SpotOptim(\n fun=ackley,\n bounds=[(-5, 5), (-5, 5)],\n restart_after_n=10,\n restart_inject_best=True,\n max_iter=30,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Best x : {result.x}\")\nprint(f\"Best f(x) : {result.fun:.6f}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nBest x : [1.37196759e-04 5.82683136e-05]\nBest f(x) : 0.000422\n```\n:::\n:::\n\n\nWith `restart_inject_best=True` (default), the best point found so far\nis injected into the new initial design after each restart.\n\n---\n\n## Parallel Evaluation\n\nSet `n_jobs` to evaluate multiple points in parallel. Use `n_jobs=-1`\nto use all available CPU cores:\n\n::: {#5d6fb967 .cell execution_count=10}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n n_jobs=2,\n eval_batch_size=2,\n max_iter=20,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Best f(x) : {result.fun:.6f}\")\nprint(f\"Evaluations: {result.nfev}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nBest f(x) : 0.000001\nEvaluations: 20\n```\n:::\n:::\n\n\n---\n\n## Configuration Access\n\nAll constructor parameters are stored in `opt.config` (a `SpotOptimConfig`\ndataclass). For convenience, you can access them directly on the optimizer:\n\n::: {#6457c2f2 .cell execution_count=11}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)], max_iter=15, seed=0)\n\nprint(f\"max_iter : {opt.max_iter}\")\nprint(f\"n_initial : {opt.n_initial}\")\nprint(f\"acquisition: {opt.acquisition}\")\nprint(f\"bounds : {opt.bounds}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nmax_iter : 15\nn_initial : 10\nacquisition: y\nbounds : [(-5.0, 5.0)]\n```\n:::\n:::\n\n\n---\n\n## See Also\n\n- [SpotOptim API](../reference/SpotOptim.SpotOptim.qmd) --- Full constructor and method documentation\n- [Surrogate Models](surrogate.qmd) --- Kriging, MLPSurrogate, custom surrogates\n- [Acquisition and Infill](optimizer.qmd) --- Acquisition functions and optimizers\n- [Built-in Test Functions](function.qmd) --- Benchmark functions for testing\n\n", + "markdown": "---\ntitle: \"The SpotOptim Class\"\ndescription: \"Central orchestrator for surrogate-model-based optimization: constructor, optimize(), and result inspection.\"\n---\n\n`SpotOptim` is the main entry point for all optimization in spotoptim.\nYou create an instance with your objective function and bounds, call\n`optimize()`, and get back a scipy-compatible `OptimizeResult`.\n\n---\n\n## Minimal Example\n\n::: {#69eeeb4f .cell execution_count=1}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=20,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Best x : {result.x}\")\nprint(f\"Best f(x) : {result.fun:.6f}\")\nprint(f\"Evaluations: {result.nfev}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nBest x : [-0.00016718 0.00071419]\nBest f(x) : 0.000001\nEvaluations: 20\n```\n:::\n:::\n\n\nThree ingredients: a callable `fun`, a list of `bounds`, and a budget via\n`max_iter` (total evaluations including the initial design). The optimizer\nbuilds a Kriging surrogate by default and uses predicted-value acquisition\n(`acquisition=\"y\"`).\n\n---\n\n## Key Constructor Parameters\n\nThe constructor accepts many parameters. The most commonly used are:\n\n| Parameter | Default | Description |\n|---|---|---|\n| `fun` | (required) | Objective function: accepts `(n, d)` array, returns `(n,)` array |\n| `bounds` | `None` | List of `(lower, upper)` tuples, one per dimension |\n| `max_iter` | `20` | Total evaluation budget (initial + sequential) |\n| `n_initial` | `10` | Number of initial design points |\n| `surrogate` | `None` | Surrogate model (default: `Kriging(method=\"regression\")`) |\n| `acquisition` | `\"y\"` | Acquisition function: `\"y\"`, `\"ei\"`, or `\"pi\"` |\n| `var_type` | `None` | Variable types: list of `\"float\"`, `\"int\"`, `\"factor\"` |\n| `seed` | `None` | Random seed for reproducibility |\n\nSee the [SpotOptim API](../reference/SpotOptim.SpotOptim.qmd) for the full parameter list.\n\n---\n\n## The OptimizeResult\n\n`optimize()` returns a `scipy.optimize.OptimizeResult` with these fields:\n\n| Field | Type | Description |\n|---|---|---|\n| `x` | `ndarray` | Best point found (original scale) |\n| `fun` | `float` | Best objective value |\n| `nfev` | `int` | Total function evaluations |\n| `nit` | `int` | Sequential iterations (after initial design) |\n| `success` | `bool` | Whether optimization succeeded |\n| `message` | `str` | Termination reason with statistics |\n| `X` | `ndarray` | All evaluated points |\n| `y` | `ndarray` | All objective values |\n\n::: {#2ee18871 .cell execution_count=2}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import ackley\n\nopt = SpotOptim(\n fun=ackley,\n bounds=[(-5, 5), (-5, 5)],\n max_iter=25,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Success : {result.success}\")\nprint(f\"Best x : {result.x}\")\nprint(f\"Best f(x) : {result.fun:.6f}\")\nprint(f\"Iterations: {result.nit}\")\nprint(f\"All points: {result.X.shape}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSuccess : True\nBest x : [ 0.00174499 -0.00130203]\nBest f(x) : 0.006284\nIterations: 15\nAll points: (25, 2)\n```\n:::\n:::\n\n\n---\n\n## Variable Types\n\nspotoptim supports three variable types via the `var_type` parameter:\n\n- `\"float\"` --- continuous real-valued variables (default)\n- `\"int\"` --- integer-constrained variables (rounded after surrogate prediction)\n- `\"factor\"` --- categorical/unordered variables (encoded internally)\n\n::: {#161aba8e .cell execution_count=3}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef mixed_objective(X):\n X = np.atleast_2d(X)\n continuous = X[:, 0]\n integer_val = X[:, 1]\n factor_val = X[:, 2]\n return continuous**2 + (integer_val - 3)**2 + factor_val\n\nopt = SpotOptim(\n fun=mixed_objective,\n bounds=[(-5.0, 5.0), (0, 10), (0, 4)],\n var_type=[\"float\", \"int\", \"factor\"],\n var_name=[\"x_cont\", \"x_int\", \"x_cat\"],\n max_iter=25,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Best x : {result.x}\")\nprint(f\"Best f(x) : {result.fun:.6f}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nBest x : [-9.98879117e-04 3.00000000e+00 0.00000000e+00]\nBest f(x) : 0.000001\n```\n:::\n:::\n\n\nWhen `var_type` is not provided, all variables default to `\"float\"`.\n\n---\n\n## Variable Transformations\n\nUse `var_trans` to apply log-transformations to parameters that span\nseveral orders of magnitude (e.g., learning rates):\n\n::: {#875f015e .cell execution_count=4}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\n\ndef obj_with_lr(X):\n X = np.atleast_2d(X)\n lr = X[:, 0]\n weight = X[:, 1]\n return (lr - 0.001)**2 + (weight - 0.5)**2\n\nopt = SpotOptim(\n fun=obj_with_lr,\n bounds=[(1e-5, 1e-1), (0.0, 1.0)],\n var_type=[\"float\", \"float\"],\n var_name=[\"lr\", \"weight\"],\n var_trans=[\"log10\", None],\n max_iter=20,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Best lr : {result.x[0]:.6f}\")\nprint(f\"Best weight : {result.x[1]:.4f}\")\nprint(f\"Best f(x) : {result.fun:.8f}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nBest lr : 0.000010\nBest weight : 0.4981\nBest f(x) : 0.00000477\n```\n:::\n:::\n\n\nWith `var_trans=[\"log10\", None]`, the first variable is optimized in\nlog10 space internally. The bounds are specified in natural (original)\nscale --- here `1e-5` to `1e-1` for the learning rate.\n\n---\n\n## Choosing an Acquisition Function\n\nThe `acquisition` parameter controls how the optimizer selects the next\nevaluation point:\n\n- `\"y\"` (default) --- minimize the surrogate's predicted value (pure exploitation)\n- `\"ei\"` --- Expected Improvement: balances exploitation and exploration\n- `\"pi\"` --- Probability of Improvement: probability of beating the current best\n\n::: {#ae1112f5 .cell execution_count=5}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import rosenbrock\n\nopt = SpotOptim(\n fun=rosenbrock,\n bounds=[(-2, 2), (-2, 2)],\n acquisition=\"ei\",\n max_iter=25,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Best x : {result.x}\")\nprint(f\"Best f(x) : {result.fun:.6f}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nBest x : [1.24915117 1.47082529]\nBest f(x) : 0.864057\n```\n:::\n:::\n\n\nSee [Acquisition and Infill](optimizer.qmd) for details on acquisition\noptimizers and infill strategies.\n\n---\n\n## Choosing a Surrogate Model\n\nThe default surrogate is `Kriging(method=\"regression\")`. You can pass\nany model that implements `fit(X, y)` and `predict(X, return_std=False)`:\n\n::: {#885cb203 .cell execution_count=6}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.surrogate import Kriging\nfrom spotoptim.function import sphere\n\nkriging = Kriging(method=\"interpolation\", noise=1e-3, seed=0)\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n surrogate=kriging,\n max_iter=20,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Best f(x) : {result.fun:.6f}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nBest f(x) : 0.006474\n```\n:::\n:::\n\n\nSee [Surrogate Models](surrogate.qmd) for Kriging options, MLPSurrogate,\nand how to plug in sklearn estimators.\n\n---\n\n## Noisy Optimization\n\nFor noisy objective functions, use `repeats_initial` and\n`repeats_surrogate` to re-evaluate each design point multiple times.\nAdd `ocba_delta` to enable Optimal Computing Budget Allocation, which\nintelligently allocates extra evaluations to the most promising points.\n\n::: {#748f7ed2 .cell execution_count=7}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import noisy_sphere\n\nnp.random.seed(0)\n\nopt = SpotOptim(\n fun=noisy_sphere,\n bounds=[(-5, 5), (-5, 5)],\n repeats_initial=3,\n repeats_surrogate=2,\n ocba_delta=3,\n max_iter=30,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Best x : {result.x}\")\nprint(f\"Best f(x) : {result.fun:.6f}\")\nprint(f\"Evaluations: {result.nfev}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nBest x : [-0.44355715 -0.46646793]\nBest f(x) : 0.395617\nEvaluations: 30\n```\n:::\n:::\n\n\nThe surrogate is fitted on the mean values across repeats, reducing\nthe effect of noise. See [Utilities](utils.qmd) for more on OCBA.\n\n---\n\n## Time Budget\n\nUse `max_time` (in minutes) to set a wall-clock time limit instead of\nor in addition to `max_iter`:\n\n::: {#874e5f35 .cell execution_count=8}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(\n fun=sphere,\n bounds=[(-5, 5), (-5, 5)],\n max_time=0.05,\n max_iter=1000,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Evaluations: {result.nfev}\")\nprint(f\"Best f(x) : {result.fun:.6f}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nEvaluations: 16\nBest f(x) : 0.000001\n```\n:::\n:::\n\n\nThe optimizer stops when either `max_iter` or `max_time` is reached,\nwhichever comes first.\n\n---\n\n## Restarts\n\nWhen the optimizer gets stuck in a local minimum, automatic restarts\ncan help. Set `restart_after_n` to trigger a restart after that many\nconsecutive iterations without improvement:\n\n::: {#d0655d10 .cell execution_count=9}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import ackley\n\nopt = SpotOptim(\n fun=ackley,\n bounds=[(-5, 5), (-5, 5)],\n restart_after_n=10,\n restart_inject_best=True,\n max_iter=30,\n n_initial=10,\n seed=0,\n)\nresult = opt.optimize()\n\nprint(f\"Best x : {result.x}\")\nprint(f\"Best f(x) : {result.fun:.6f}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nBest x : [1.37196759e-04 5.82683136e-05]\nBest f(x) : 0.000422\n```\n:::\n:::\n\n\nWith `restart_inject_best=True` (default), the best point found so far\nis injected into the new initial design after each restart.\n\n---\n\n## Configuration Access\n\nAll constructor parameters are stored in `opt.config` (a `SpotOptimConfig`\ndataclass). For convenience, you can access them directly on the optimizer:\n\n::: {#81bdf1e4 .cell execution_count=10}\n``` {.python .cell-code}\nfrom spotoptim import SpotOptim\nfrom spotoptim.function import sphere\n\nopt = SpotOptim(fun=sphere, bounds=[(-5, 5)], max_iter=15, seed=0)\n\nprint(f\"max_iter : {opt.max_iter}\")\nprint(f\"n_initial : {opt.n_initial}\")\nprint(f\"acquisition: {opt.acquisition}\")\nprint(f\"bounds : {opt.bounds}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nmax_iter : 15\nn_initial : 10\nacquisition: y\nbounds : [(-5.0, 5.0)]\n```\n:::\n:::\n\n\n---\n\n## See Also\n\n- [SpotOptim API](../reference/SpotOptim.SpotOptim.qmd) --- Full constructor and method documentation\n- [Surrogate Models](surrogate.qmd) --- Kriging, MLPSurrogate, custom surrogates\n- [Acquisition and Infill](optimizer.qmd) --- Acquisition functions and optimizers\n- [Built-in Test Functions](function.qmd) --- Benchmark functions for testing\n\n", "supporting": [ - "spotoptim_files" + "spotoptim_files/figure-html" ], "filters": [], "includes": {} diff --git a/_freeze/docs/user_guide/utils/execute-results/html.json b/_freeze/docs/user_guide/utils/execute-results/html.json index 4eada605..ec2b09c7 100644 --- a/_freeze/docs/user_guide/utils/execute-results/html.json +++ b/_freeze/docs/user_guide/utils/execute-results/html.json @@ -1,10 +1,10 @@ { - "hash": "b919af4f7ead2500e6103440240721c0", + "hash": "fc0f9dde2ff45caa386f86503df9443f", "result": { "engine": "jupyter", - "markdown": "---\ntitle: \"Utilities\"\ndescription: \"Boundaries, transformations, PCA, OCBA, scaling, and parallel helpers.\"\n---\n\nspotoptim ships a collection of utility functions that support the\noptimization loop and post-hoc analysis. This page covers the most\ncommonly used helpers in `spotoptim.utils`.\n\n---\n\n## Boundaries and Mapping\n\n**`get_boundaries`** computes the column-wise minimum and maximum of a\nNumPy array. This is useful for determining the range of evaluated\npoints or for setting up scaling.\n\n`map_to_original_scale` maps points from the $[0, 1]$ unit hypercube\nback to the original variable ranges defined by lower and upper bounds.\n\n::: {#3371e3d8 .cell execution_count=1}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim.utils import get_boundaries, map_to_original_scale\n\nnp.random.seed(0)\ndata = np.random.uniform(low=-5, high=5, size=(20, 3))\n\nmin_vals, max_vals = get_boundaries(data)\nprint(f\"Min per column: {min_vals}\")\nprint(f\"Max per column: {max_vals}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nMin per column: [-4.128707 -4.812102 -4.28963942]\nMax per column: [4.44668917 4.88373838 4.78618342]\n```\n:::\n:::\n\n\nGiven boundaries, you can map unit-scaled search points back to the\noriginal scale:\n\n::: {#6e6b56c8 .cell execution_count=2}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim.utils import map_to_original_scale\n\nx_min = np.array([0.0, -10.0])\nx_max = np.array([10.0, 10.0])\nX_unit = np.array([[0.0, 0.5], [0.25, 0.75], [1.0, 1.0]])\n\nX_original = map_to_original_scale(X_unit, x_min, x_max)\nprint(X_original)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[[ 0. 0. ]\n [ 2.5 5. ]\n [10. 10. ]]\n```\n:::\n:::\n\n\n---\n\n## PCA Utilities\n\nThe `get_pca` function scales numeric columns of a DataFrame and\nperforms Principal Component Analysis. It returns the fitted PCA object,\nscaled data, feature names, sample names, and the transformed data.\n\n`get_pca_topk` identifies the top $k$ features with the strongest\ninfluence on PC1 and PC2.\n\n::: {#75ea04ce .cell execution_count=3}\n``` {.python .cell-code}\nimport numpy as np\nimport pandas as pd\nfrom spotoptim.utils import get_pca, get_pca_topk\n\nnp.random.seed(0)\ndf = pd.DataFrame({\n \"feature_a\": np.random.randn(50),\n \"feature_b\": np.random.randn(50) * 2,\n \"feature_c\": np.random.randn(50) + 1,\n \"feature_d\": np.random.randn(50) * 0.5,\n})\n\npca, scaled_data, feature_names, sample_names, pca_data = get_pca(df, n_components=3)\n\nprint(f\"Feature names: {list(feature_names)}\")\nprint(f\"Explained variance: {pca.explained_variance_ratio_}\")\nprint(f\"PCA data shape: {pca_data.shape}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nFeature names: ['feature_a', 'feature_b', 'feature_c', 'feature_d']\nExplained variance: [0.54774396 0.22793607 0.18192548]\nPCA data shape: (50, 3)\n```\n:::\n:::\n\n\nUse `get_pca_topk` to find which original features load most heavily on\nthe first two components:\n\n::: {#3299b6ad .cell execution_count=4}\n``` {.python .cell-code}\nimport numpy as np\nimport pandas as pd\nfrom spotoptim.utils import get_pca, get_pca_topk\n\nnp.random.seed(0)\ndf = pd.DataFrame({\n \"feature_a\": np.random.randn(50),\n \"feature_b\": np.random.randn(50) * 2,\n \"feature_c\": np.random.randn(50) + 1,\n \"feature_d\": np.random.randn(50) * 0.5,\n})\n\npca, _, feature_names, _, _ = get_pca(df, n_components=2)\ntop_pc1, top_pc2 = get_pca_topk(pca, feature_names, k=2)\n\nprint(f\"Top features for PC1: {top_pc1}\")\nprint(f\"Top features for PC2: {top_pc2}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTop features for PC1: ['feature_b', 'feature_c']\nTop features for PC2: ['feature_a', 'feature_c']\n```\n:::\n:::\n\n\n---\n\n## OCBA (Optimal Computing Budget Allocation)\n\nWhen the objective function is noisy, repeated evaluations of the same\ndesign can be allocated smartly using **OCBA**. Given the current sample\nmeans, variances, and an incremental budget $\\delta$, `get_ocba`\nreturns an allocation vector that concentrates evaluations on the most\npromising and most uncertain designs.\n\n`get_ranks` is a helper that returns the rank of each element in an\narray (0 = smallest).\n\n::: {#23787a0e .cell execution_count=5}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim.utils.ocba import get_ocba, get_ranks\n\nmeans = np.array([2.1, 3.5, 1.8, 4.0, 2.9])\nvariances = np.array([0.5, 1.2, 0.3, 0.8, 1.0])\ndelta = 20\n\nranks = get_ranks(means)\nprint(f\"Ranks: {ranks}\")\n\nallocation = get_ocba(means, variances, delta)\nprint(f\"OCBA allocation (delta={delta}): {allocation}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nRanks: [1 3 0 4 2]\nOCBA allocation (delta=20): [10 1 8 0 1]\n```\n:::\n:::\n\n\nThe allocation vector tells you how many additional evaluations each\ndesign should receive. Designs with lower means (better objectives,\nassuming minimization) and higher variance tend to receive more budget.\n\nSee [The SpotOptim Class](spotoptim.qmd) for how OCBA integrates into\nnoisy optimization runs.\n\n---\n\n## TorchStandardScaler\n\n`TorchStandardScaler` standardizes PyTorch tensors to zero mean and unit\nvariance, analogous to sklearn's `StandardScaler` but operating on\n`torch.Tensor` objects directly.\n\n::: {#a9aec684 .cell execution_count=6}\n``` {.python .cell-code}\nimport torch\nfrom spotoptim.utils import TorchStandardScaler\n\ntorch.manual_seed(0)\nX = torch.randn(10, 3) * 5 + 2 # mean ~2, std ~5\n\nscaler = TorchStandardScaler()\nscaler.fit(X)\n\nX_scaled = scaler.transform(X)\nprint(f\"Original mean: {X.mean(dim=0).tolist()}\")\nprint(f\"Scaled mean: {X_scaled.mean(dim=0).tolist()}\")\nprint(f\"Scaled std: {X_scaled.std(dim=0).tolist()}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nOriginal mean: [0.2195490151643753, 1.6389217376708984, 2.548074722290039]\nScaled mean: [5.9604645663569045e-09, -4.470348358154297e-08, 2.3841858265427618e-08]\nScaled std: [1.054092526435852, 1.0540926456451416, 1.0540926456451416]\n```\n:::\n:::\n\n\nThe `fit_transform` shortcut fits and transforms in a single call:\n\n::: {#0f82c790 .cell execution_count=7}\n``` {.python .cell-code}\nimport torch\nfrom spotoptim.utils import TorchStandardScaler\n\ntorch.manual_seed(0)\nX = torch.randn(8, 2) * 3\n\nscaler = TorchStandardScaler()\nX_scaled = scaler.fit_transform(X)\nprint(f\"Shape: {X_scaled.shape}\")\nprint(f\"Mean after scaling: {X_scaled.mean(dim=0)}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nShape: torch.Size([8, 2])\nMean after scaling: tensor([ 3.7253e-08, -2.9802e-08])\n```\n:::\n:::\n\n\n---\n\n## Parallel Evaluation\n\n`is_gil_disabled` checks whether the current Python interpreter is a\nfree-threaded build (PEP 703). On standard CPython the GIL is enabled\nand this returns `False`. spotoptim uses this check internally to decide\nwhether thread-based parallelism is safe for objective evaluation.\n\n::: {#d50a71fe .cell execution_count=8}\n``` {.python .cell-code}\nfrom spotoptim.utils import is_gil_disabled\n\nresult = is_gil_disabled()\nprint(f\"GIL disabled: {result}\")\nprint(f\"Return type: {type(result).__name__}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nGIL disabled: False\nReturn type: bool\n```\n:::\n:::\n\n\n", + "markdown": "---\ntitle: \"Utilities\"\ndescription: \"Boundaries, transformations, PCA, OCBA, and scaling helpers.\"\n---\n\nspotoptim ships a collection of utility functions that support the\noptimization loop and post-hoc analysis. This page covers the most\ncommonly used helpers in `spotoptim.utils`.\n\n---\n\n## Boundaries and Mapping\n\n**`get_boundaries`** computes the column-wise minimum and maximum of a\nNumPy array. This is useful for determining the range of evaluated\npoints or for setting up scaling.\n\n`map_to_original_scale` maps points from the $[0, 1]$ unit hypercube\nback to the original variable ranges defined by lower and upper bounds.\n\n::: {#81f3cd23 .cell execution_count=1}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim.utils import get_boundaries, map_to_original_scale\n\nnp.random.seed(0)\ndata = np.random.uniform(low=-5, high=5, size=(20, 3))\n\nmin_vals, max_vals = get_boundaries(data)\nprint(f\"Min per column: {min_vals}\")\nprint(f\"Max per column: {max_vals}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nMin per column: [-4.128707 -4.812102 -4.28963942]\nMax per column: [4.44668917 4.88373838 4.78618342]\n```\n:::\n:::\n\n\nGiven boundaries, you can map unit-scaled search points back to the\noriginal scale:\n\n::: {#9b48c1db .cell execution_count=2}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim.utils import map_to_original_scale\n\nx_min = np.array([0.0, -10.0])\nx_max = np.array([10.0, 10.0])\nX_unit = np.array([[0.0, 0.5], [0.25, 0.75], [1.0, 1.0]])\n\nX_original = map_to_original_scale(X_unit, x_min, x_max)\nprint(X_original)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[[ 0. 0. ]\n [ 2.5 5. ]\n [10. 10. ]]\n```\n:::\n:::\n\n\n---\n\n## PCA Utilities\n\nThe `get_pca` function scales numeric columns of a DataFrame and\nperforms Principal Component Analysis. It returns the fitted PCA object,\nscaled data, feature names, sample names, and the transformed data.\n\n`get_pca_topk` identifies the top $k$ features with the strongest\ninfluence on PC1 and PC2.\n\n::: {#d66925e5 .cell execution_count=3}\n``` {.python .cell-code}\nimport numpy as np\nimport pandas as pd\nfrom spotoptim.utils import get_pca, get_pca_topk\n\nnp.random.seed(0)\ndf = pd.DataFrame({\n \"feature_a\": np.random.randn(50),\n \"feature_b\": np.random.randn(50) * 2,\n \"feature_c\": np.random.randn(50) + 1,\n \"feature_d\": np.random.randn(50) * 0.5,\n})\n\npca, scaled_data, feature_names, sample_names, pca_data = get_pca(df, n_components=3)\n\nprint(f\"Feature names: {list(feature_names)}\")\nprint(f\"Explained variance: {pca.explained_variance_ratio_}\")\nprint(f\"PCA data shape: {pca_data.shape}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nFeature names: ['feature_a', 'feature_b', 'feature_c', 'feature_d']\nExplained variance: [0.54774396 0.22793607 0.18192548]\nPCA data shape: (50, 3)\n```\n:::\n:::\n\n\nUse `get_pca_topk` to find which original features load most heavily on\nthe first two components:\n\n::: {#ebc5d648 .cell execution_count=4}\n``` {.python .cell-code}\nimport numpy as np\nimport pandas as pd\nfrom spotoptim.utils import get_pca, get_pca_topk\n\nnp.random.seed(0)\ndf = pd.DataFrame({\n \"feature_a\": np.random.randn(50),\n \"feature_b\": np.random.randn(50) * 2,\n \"feature_c\": np.random.randn(50) + 1,\n \"feature_d\": np.random.randn(50) * 0.5,\n})\n\npca, _, feature_names, _, _ = get_pca(df, n_components=2)\ntop_pc1, top_pc2 = get_pca_topk(pca, feature_names, k=2)\n\nprint(f\"Top features for PC1: {top_pc1}\")\nprint(f\"Top features for PC2: {top_pc2}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nTop features for PC1: ['feature_b', 'feature_c']\nTop features for PC2: ['feature_a', 'feature_c']\n```\n:::\n:::\n\n\n---\n\n## OCBA (Optimal Computing Budget Allocation)\n\nWhen the objective function is noisy, repeated evaluations of the same\ndesign can be allocated smartly using **OCBA**. Given the current sample\nmeans, variances, and an incremental budget $\\delta$, `get_ocba`\nreturns an allocation vector that concentrates evaluations on the most\npromising and most uncertain designs.\n\n`get_ranks` is a helper that returns the rank of each element in an\narray (0 = smallest).\n\n::: {#d833b4ab .cell execution_count=5}\n``` {.python .cell-code}\nimport numpy as np\nfrom spotoptim.utils.ocba import get_ocba, get_ranks\n\nmeans = np.array([2.1, 3.5, 1.8, 4.0, 2.9])\nvariances = np.array([0.5, 1.2, 0.3, 0.8, 1.0])\ndelta = 20\n\nranks = get_ranks(means)\nprint(f\"Ranks: {ranks}\")\n\nallocation = get_ocba(means, variances, delta)\nprint(f\"OCBA allocation (delta={delta}): {allocation}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nRanks: [1 3 0 4 2]\nOCBA allocation (delta=20): [10 1 8 0 1]\n```\n:::\n:::\n\n\nThe allocation vector tells you how many additional evaluations each\ndesign should receive. Designs with lower means (better objectives,\nassuming minimization) and higher variance tend to receive more budget.\n\nSee [The SpotOptim Class](spotoptim.qmd) for how OCBA integrates into\nnoisy optimization runs.\n\n---\n\n## TorchStandardScaler\n\n`TorchStandardScaler` standardizes PyTorch tensors to zero mean and unit\nvariance, analogous to sklearn's `StandardScaler` but operating on\n`torch.Tensor` objects directly.\n\n::: {#be51d7f4 .cell execution_count=6}\n``` {.python .cell-code}\nimport torch\nfrom spotoptim.utils import TorchStandardScaler\n\ntorch.manual_seed(0)\nX = torch.randn(10, 3) * 5 + 2 # mean ~2, std ~5\n\nscaler = TorchStandardScaler()\nscaler.fit(X)\n\nX_scaled = scaler.transform(X)\nprint(f\"Original mean: {X.mean(dim=0).tolist()}\")\nprint(f\"Scaled mean: {X_scaled.mean(dim=0).tolist()}\")\nprint(f\"Scaled std: {X_scaled.std(dim=0).tolist()}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nOriginal mean: [0.2195490151643753, 1.6389217376708984, 2.548074722290039]\nScaled mean: [5.9604645663569045e-09, -4.470348358154297e-08, 2.3841858265427618e-08]\nScaled std: [1.054092526435852, 1.0540926456451416, 1.0540926456451416]\n```\n:::\n:::\n\n\nThe `fit_transform` shortcut fits and transforms in a single call:\n\n::: {#ad087f23 .cell execution_count=7}\n``` {.python .cell-code}\nimport torch\nfrom spotoptim.utils import TorchStandardScaler\n\ntorch.manual_seed(0)\nX = torch.randn(8, 2) * 3\n\nscaler = TorchStandardScaler()\nX_scaled = scaler.fit_transform(X)\nprint(f\"Shape: {X_scaled.shape}\")\nprint(f\"Mean after scaling: {X_scaled.mean(dim=0)}\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nShape: torch.Size([8, 2])\nMean after scaling: tensor([ 3.7253e-08, -2.9802e-08])\n```\n:::\n:::\n\n\n", "supporting": [ - "utils_files" + "utils_files/figure-html" ], "filters": [], "includes": {} diff --git a/_quarto.yml b/_quarto.yml index f3abefbc..c601b39d 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -89,12 +89,8 @@ website: file: docs/spotoptim_init.qmd - text: Sequential Optimization file: docs/optimize_seq.qmd - - text: Parallel Optimization - file: docs/optimize_parallel.qmd - text: Early Stopping file: docs/early-stopping.qmd - - text: Running on Slurm (GWDG NHR) - file: docs/slurm.qmd - section: API Reference contents: - text: Overview diff --git a/docs/early-stopping.qmd b/docs/early-stopping.qmd index 56c116bc..916e35b0 100644 --- a/docs/early-stopping.qmd +++ b/docs/early-stopping.qmd @@ -179,10 +179,5 @@ features — they prune *inside* a multi-trial ML training run, whereas * [Sequential Optimization](optimize_seq.qmd) — outer restart loop and `execute_optimization_run()`. -* [Parallel Optimization](optimize_parallel.qmd) — `max_restarts` fires - identically under `n_jobs>1` steady-state parallelism. -* [Running on Slurm (GWDG NHR)](slurm.qmd) — bake `max_restarts` into the - experiment pickle to avoid re-dispatching a job once the optimizer has - plateaued. * `SpotOptim.SpotOptimConfig` in the API reference — the authoritative parameter list. diff --git a/docs/optimize_parallel.qmd b/docs/optimize_parallel.qmd deleted file mode 100644 index d536143e..00000000 --- a/docs/optimize_parallel.qmd +++ /dev/null @@ -1,725 +0,0 @@ ---- -title: "SpotOptim: Parallel Optimization" -description: "Step-by-step walkthrough of execute_optimization_run() and every method it calls along the parallel (steady-state) path, with executable examples validated by pytest." ---- - -This document traces every step executed by `SpotOptim.execute_optimization_run()` along -the parallel code path (`n_jobs > 1`), in the order they occur. -Each section describes one method or phase with a `{python}` code block that can be executed directly. - -The public entry point is `optimize()`, which manages the outer restart loop and delegates -each cycle to `execute_optimization_run()`. -When `n_jobs > 1`, that dispatcher routes to `optimize_steady_state()`, -which implements a hybrid steady-state parallelisation strategy that overlaps surrogate -search with objective function evaluation. -This document covers that path in full. - -Run all related tests with: - -```bash -uv run pytest tests/test_spotoptim_deep.py -v -``` - ---- - -## Step 1 — Dispatch (`execute_optimization_run()`) - -```python -if self.n_jobs > 1: - return self.optimize_steady_state(...) -else: - return self.optimize_sequential_run(...) -``` - -`execute_optimization_run()` is the routing layer between the outer restart loop in -`optimize()` and the actual optimisation engine. -Its sole responsibility is to examine `n_jobs` and forward all arguments to either -`optimize_steady_state()` (parallel) or `optimize_sequential_run()` (sequential). -It returns a `(status, OptimizeResult)` tuple in both cases, which `optimize()` uses to -decide whether to restart or terminate. -The optional `shared_best_y` and `shared_lock` parameters support inter-worker -coordination; they are unused in the current steady-state implementation and reserved -for future multi-restart parallelism. - -```{python} -import time -import numpy as np -from spotoptim import SpotOptim -from spotoptim.function import sphere - -opt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], - n_initial=5, max_iter=10, seed=0, n_jobs=2) -status, result = opt.execute_optimization_run(timeout_start=time.time()) -print(f"status : {status}") -print(f"best : {result.fun:.6f}") -assert status == "FINISHED" -print("dispatch check passed.") -``` - ---- - -## Step 2 — Parallel Run Orchestration (`optimize_steady_state()`) - -```python -self.set_seed() -X0 = self.get_initial_design(X0) -X0 = self.curate_initial_design(X0) -# Phase 1: parallel initial evaluation -# Phase 2: steady-state loop -return "FINISHED", OptimizeResult(...) -``` - -`optimize_steady_state()` is the parallel orchestrator. -It follows the same preparatory sequence as its sequential counterpart — seeding the -random number generator, generating and curating the initial design — before switching -to a pool-based execution model. -The method never returns `"RESTART"`: unlike the sequential loop, the steady-state engine -does not implement a zero-success-rate restart criterion, and always exits with -`"FINISHED"`. -All worker pools are managed inside a single `contextlib.ExitStack` that guarantees -orderly shutdown on success and on exception. - -```{python} -import time -from spotoptim import SpotOptim -from spotoptim.function import sphere - -opt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], - n_initial=5, max_iter=10, seed=0, n_jobs=2) -status, result = opt.optimize_steady_state(timeout_start=time.time(), X0=None) -print(f"status : {status}") -print(f"evaluations : {result.nfev}") -print(f"best : {result.fun:.6f}") -assert status == "FINISHED" -print("parallel orchestration check passed.") -``` - ---- - -## Step 3 — Seed and Initial Design (`set_seed()`, `get_initial_design()`, `curate_initial_design()`) - -```python -self.set_seed() -X0 = self.get_initial_design(X0) -X0 = self.curate_initial_design(X0) -``` - -The parallel path begins with the same three preparatory calls as the sequential path. -`set_seed()` re-seeds Python's `random` module and NumPy's global generator to ensure -reproducibility. -`get_initial_design()` either processes the user-supplied `X0` or generates a Latin -Hypercube sample in the transformed, reduced search space. -`curate_initial_design()` removes duplicate points and generates replacements as needed. -Because points are dispatched concurrently to worker processes or threads in the next -phase, curation must be complete before submission: worker processes operate on a -snapshot of the optimiser serialised with `dill` and cannot interact with the -main-process state. -Immediately after curation, restart injection is applied: a `y0_prefilled` array (all -`NaN` by default) is populated with `y0_known` at the position of the matching `self.x0` -point, using the same distance tolerance as `_initialize_run()` in the sequential path. -This pre-filled array is consumed by Phase 1 to skip one pool submission per restart. - -```{python} -import numpy as np -from spotoptim import SpotOptim -from spotoptim.function import sphere - -opt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], n_initial=5, seed=42, n_jobs=2) -opt.set_seed() -X0 = opt.get_initial_design(None) -X0 = opt.curate_initial_design(X0) -print(f"initial design shape : {X0.shape}") -assert X0.shape == (5, 2) -assert len(np.unique(X0, axis=0)) == 5 -print("seed and initial design check passed.") -``` - ---- - -## Step 4 — GIL Detection and Executor Construction (`is_gil_disabled()`) - -```python -_no_gil = is_gil_disabled() -with ExitStack() as _stack: - eval_pool = _stack.enter_context( - ThreadPoolExecutor(max_workers=self.n_jobs) if _no_gil - else ProcessPoolExecutor(max_workers=self.n_jobs) - ) - search_pool = _stack.enter_context( - ThreadPoolExecutor(max_workers=self.n_jobs) - ) -``` - -`is_gil_disabled()` queries `sys._is_gil_enabled()` (Python 3.13+) to detect whether -the interpreter was built without the Global Interpreter Lock. -On standard GIL builds — Python 3.12 or GIL-enabled 3.13 — objective evaluations are -dispatched to a `ProcessPoolExecutor` so that each worker runs in a separate process, -giving true CPU-level parallelism and safe isolation of arbitrary callables. -Surrogate search tasks are always dispatched to a `ThreadPoolExecutor` because they -share the main-process heap and require no serialisation. -On free-threaded builds (`python3.13t`) both pools are `ThreadPoolExecutor` instances: -threads achieve true parallelism without the GIL, `dill` serialisation is eliminated, -and the objective function is called directly from the shared heap. -The `_surrogate_lock` (a `threading.Lock`) is used in both configurations to serialise -concurrent surrogate reads and refits. - -```{python} -from spotoptim.utils.parallel import is_gil_disabled - -result = is_gil_disabled() -print(f"GIL disabled: {result}") -assert isinstance(result, bool) -print("GIL detection check passed.") -``` - ---- - -## Step 5 — Phase 1: Parallel Initial Submission - -```python -n_to_submit = 0 -for i, x in enumerate(X0): - if np.isfinite(y0_prefilled[i]): - # Restart injection: store directly, skip the pool. - self._update_storage_steady(x, y0_prefilled[i]) - continue - if _no_gil: - fut = eval_pool.submit(_thread_eval_task_single, x) - else: - pickled_args = dill.dumps((self, x)) - fut = eval_pool.submit(remote_eval_wrapper, pickled_args) - futures[fut] = "eval" - n_to_submit += 1 -``` - -The initial design is partitioned into two groups before any worker is touched. -Points that carry a pre-filled `y0_prefilled` value — set during Step 4 for the -restart-injected best point — are stored on the main thread via -`_update_storage_steady()` and skipped entirely. -The remaining `n_to_submit` points are submitted to `eval_pool` concurrently. -On GIL builds each point is serialised together with a snapshot of the optimiser using -`dill.dumps()`; the TensorBoard writer is temporarily set to `None` before serialisation -because `SummaryWriter` objects are not picklable. -`remote_eval_wrapper()` unpickles the arguments in the worker process, reshapes the -point to a `(1, d)` array, calls `evaluate_function()`, and returns the `(x, y)` pair. -On free-threaded builds `_thread_eval_task_single()` calls `evaluate_function()` without -any serialisation overhead. -All submitted futures are tracked in a `futures` dictionary keyed by `Future` object -with the string tag `"eval"`. -When `y0_known` is `None` (no restart), `y0_prefilled` is all-NaN and `n_to_submit` -equals `n_initial`, so behaviour is identical to a fresh run. - -```{python} -import time -import numpy as np -from spotoptim import SpotOptim -from spotoptim.function import sphere - -# Fresh run — all n_initial points are submitted to the pool. -opt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], - n_initial=8, max_iter=8, seed=0, n_jobs=2) -result = opt.optimize() -assert result.nfev == 8 -print(f"evaluations (no injection): {result.nfev}") - -# Restart injection — the known best point is stored directly, not re-evaluated. -x_inject = np.array([0.5, -0.5]) -opt2 = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], - n_initial=6, max_iter=6, seed=1, n_jobs=2, - x0=x_inject) -opt2.optimize_steady_state( - timeout_start=time.time(), - X0=None, - y0_known=float(np.sum(x_inject**2)), -) -assert float(np.sum(x_inject**2)) in opt2.y_ -print(f"injected value present in y_: {float(np.sum(x_inject**2)):.4f}") -print("parallel initial submission check passed.") -``` - ---- - -## Step 6 — Initial Design Collection (`_update_storage_steady()`) - -```python -while initial_done_count < len(X0): - done, _ = wait(futures.keys(), return_when=FIRST_COMPLETED) - for fut in done: - ftype = futures.pop(fut) - if ftype != "eval": - continue - x_done, y_done = fut.result() - if not isinstance(y_done, Exception): - self._update_storage_steady(x_done, y_done) - initial_done_count += 1 -``` - -The main thread waits in a loop using `concurrent.futures.wait()` with -`return_when=FIRST_COMPLETED`, processing each completed future as it arrives. -Futures tagged `"eval"` are the only type active during Phase 1; any unexpected type -is silently skipped. -When a result is an `Exception` instance — indicating a worker-side failure — the point -is dropped and, if `verbose=True`, the error is printed; the initial design count still -advances so the loop terminates correctly. -For valid results, `_update_storage_steady()` appends the point in original scale to -`X_` and its objective value to `y_`, initialising both arrays on the first call. -It also updates `best_x_`, `best_y_`, `min_y`, and `min_X` in-place whenever the new -value improves on the current best. -Because the main thread is the only writer during Phase 1, no locking is required here. - -```{python} -import numpy as np -from spotoptim import SpotOptim -from spotoptim.function import sphere - -opt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], - n_initial=5, max_iter=5, seed=0, n_jobs=2) -opt.optimize() -print(f"X_ shape : {opt.X_.shape}") -print(f"y_ : {opt.y_}") -assert opt.X_.shape[0] >= 1 -assert np.isfinite(opt.best_y_) -print("_update_storage_steady check passed.") -``` - ---- - -## Step 7 — Initial Postprocessing (`_init_tensorboard()`, `update_stats()`, `get_best_xy_initial_design()`) - -```python -self._init_tensorboard() -if self.y_ is None or len(self.y_) == 0: - raise RuntimeError(...) -self.update_stats() -self.get_best_xy_initial_design() -``` - -Once all initial evaluations have completed, three postprocessing steps are applied in -fixed order. -`_init_tensorboard()` logs each initial-design point to TensorBoard as a separate -hyperparameter run; when `tensorboard_log=False` (the default) it is a no-op. -A guard check follows: if `y_` is `None` or empty, every worker evaluation failed and -a `RuntimeError` is raised with a diagnostic message pointing to likely causes such as -unpicklable callables or missing imports inside the worker process. -`update_stats()` refreshes `min_y`, `min_X`, `counter`, and, for noisy objectives, -aggregated per-point means and variances. -`get_best_xy_initial_design()` identifies the initial best solution and writes it to -`best_x_` and `best_y_`; unlike the sequential path, these attributes are updated -incrementally by `_update_storage_steady()` throughout the loop, so this call aligns -the verbose best-solution display with the true running best after Phase 1. - -```{python} -from spotoptim import SpotOptim -from spotoptim.function import sphere - -opt = SpotOptim(fun=sphere, bounds=[(-5, 5)], tensorboard_log=False, - n_initial=5, max_iter=5, seed=0, n_jobs=2) -opt.optimize() -assert opt.tb_writer is None -assert opt.counter == 5 -print(f"counter : {opt.counter}") -print(f"min_y : {opt.min_y:.6f}") -print("initial postprocessing check passed.") -``` - ---- - -## Step 8 — First Surrogate Fit (`fit_scheduler()`) - -```python -# No lock needed — no search threads active yet -self.fit_scheduler() -``` - -After the initial postprocessing, the surrogate model is fitted to the complete initial -design for the first time. -No surrogate lock is acquired here because the `search_pool` has not yet been populated: -this is the only point in the parallel path where `fit_scheduler()` is called without -holding `_surrogate_lock`. -`fit_scheduler()` selects the most recent `window_size` training points according to -`selection_method`, fits the surrogate, and prepares it for acquisition-function -queries. -When a list of surrogates was specified at construction, one is chosen probabilistically -according to `prob_surrogate` before fitting. - -```{python} -import time -from spotoptim import SpotOptim -from spotoptim.function import sphere - -opt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], - n_initial=5, max_iter=15, window_size=10, seed=0, n_jobs=2) -result = opt.optimize() -print(f"window_size : {opt.window_size}") -print(f"evaluations : {result.nfev}") -assert opt.window_size == 10 -print("first surrogate fit check passed.") -``` - ---- - -## Step 9 — Steady-State Loop Overview (`optimize_steady_state()`) - -```python -pending_cands: list = [] -_future_n_pts: dict = {} - -while (len(self.y_) < effective_max_iter) and ( - time.time() < timeout_start + self.max_time * 60 -): - if _batch_ready(): - _flush_batch() - # fill open slots with search tasks - # flush again if threshold crossed - # wait for any future to complete - # route result by ftype: "search" or "batch_eval" -``` - -The main iteration loop runs until either the evaluation budget `effective_max_iter` or -the wall-clock limit `max_time` minutes is exhausted. -Two data structures coordinate the flow: `pending_cands` accumulates candidates returned -by completed search tasks, and `_future_n_pts` maps each in-flight batch-eval future to -the number of points it carries. -Both structures are required for accurate budget accounting: the reserved count is -`len(y_) + n_in_flight + n_active_searches + len(pending_cands)`, ensuring the total -never exceeds `effective_max_iter` regardless of concurrency. -Each loop iteration performs four actions in order — batch flush, slot filling, second -flush, and future wait — before routing completed results by their type tag. - -```{python} -import numpy as np -from spotoptim import SpotOptim -from spotoptim.function import sphere - -opt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], - n_initial=5, max_iter=20, seed=0, n_jobs=2) -result = opt.optimize() -print(f"evaluations : {result.nfev}") -print(f"best : {result.fun:.6f}") -assert result.nfev <= 20 -assert result.success -print("steady-state loop check passed.") -``` - ---- - -## Step 10 — Batch Readiness (`_batch_ready()`) - -```python -def _batch_ready() -> bool: - if not pending_cands: - return False - if len(pending_cands) >= self.eval_batch_size: - return True - return not any(t == "search" for t in futures.values()) -``` - -`_batch_ready()` determines whether the accumulated candidates in `pending_cands` should -be dispatched as a batch evaluation. -The primary condition is that the number of pending candidates meets or exceeds -`eval_batch_size`. -A secondary starvation-guard condition also triggers a flush when no search tasks remain -in flight: without this guard, pending candidates would block indefinitely if the budget -is nearly exhausted and no further search tasks can be submitted. -With `eval_batch_size=1` (the default), `_batch_ready()` returns `True` whenever any -candidate is pending, preserving the one-point-at-a-time behaviour of the sequential -path. -Larger values of `eval_batch_size` amortise process-spawn and inter-process -communication overhead across multiple points, improving throughput for expensive -objectives on GIL builds. - -```{python} -import numpy as np -from spotoptim import SpotOptim -from spotoptim.function import sphere - -# Default batch size = 1: each search result is dispatched immediately -opt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], - n_initial=5, max_iter=15, seed=0, n_jobs=2, eval_batch_size=1) -result = opt.optimize() -assert opt.eval_batch_size == 1 -print(f"eval_batch_size : {opt.eval_batch_size}") -print(f"evaluations : {result.nfev}") -print("_batch_ready check passed.") -``` - ---- - -## Step 11 — Batch Dispatch (`_flush_batch()`) - -```python -def _flush_batch() -> None: - X_batch = np.vstack(pending_cands) - n_in_batch = len(pending_cands) - pending_cands.clear() - if _no_gil: - fut_eval = eval_pool.submit(_thread_batch_eval_task, X_batch) - else: - pickled_args = dill.dumps((self, X_batch)) - fut_eval = eval_pool.submit(remote_batch_eval_wrapper, pickled_args) - futures[fut_eval] = "batch_eval" - _future_n_pts[fut_eval] = n_in_batch -``` - -`_flush_batch()` stacks all pending candidates into a single `(n, d)` array `X_batch`, -clears `pending_cands`, and dispatches the batch to `eval_pool` as one future. -On GIL builds `remote_batch_eval_wrapper()` is used: it unpickles the optimiser and -batch in the worker process, calls `evaluate_function(X_batch)` once, and returns -`(X_batch, y_batch)`. -Evaluating the whole batch in a single `fun()` call avoids repeated process-spawn -overhead and allows vectorised objective implementations to exploit NumPy-level -parallelism within the worker. -On free-threaded builds `_thread_batch_eval_task()` performs the same operation -directly in a thread without serialisation. -The future is registered under the `"batch_eval"` tag and its point count is recorded in -`_future_n_pts` so that budget accounting remains correct while the evaluation is -in flight. - -```{python} -import numpy as np -from spotoptim import SpotOptim -from spotoptim.function import sphere - -opt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], - n_initial=5, max_iter=20, seed=0, n_jobs=2, eval_batch_size=3) -result = opt.optimize() -assert opt.eval_batch_size == 3 -print(f"eval_batch_size : {opt.eval_batch_size}") -print(f"evaluations : {result.nfev}") -print("_flush_batch check passed.") -``` - ---- - -## Step 12 — Search Slot Management (`_thread_search_task()`) - -```python -n_in_flight = sum(_future_n_pts.values()) -n_searches = sum(1 for t in futures.values() if t == "search") -reserved = len(self.y_) + n_in_flight + n_searches + len(pending_cands) -if reserved < effective_max_iter: - fut = search_pool.submit(_thread_search_task) - futures[fut] = "search" -``` - -At each loop iteration, the main thread fills any open slots in the search pool up to -`n_jobs` concurrent tasks. -Before submitting a new search task, the budget guard checks that adding one more -in-flight search would not push the reserved count over `effective_max_iter`: if it -would, no further search tasks are submitted and the loop drains existing futures until -the budget is consumed. -`_thread_search_task()` is a nested closure that acquires `_surrogate_lock` before -calling `suggest_next_infill_point()` and releases it on return, so that a concurrent -surrogate refit on the main thread cannot corrupt the model while a search thread reads -it. -Because search tasks are always submitted to a `ThreadPoolExecutor`, they share the main -process heap and require no serialisation regardless of the GIL state. - -```{python} -import numpy as np -from spotoptim import SpotOptim -from spotoptim.function import sphere - -opt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], - n_initial=5, max_iter=12, seed=0, n_jobs=3) -result = opt.optimize() -assert result.nfev <= 12 -print(f"n_jobs : {opt.n_jobs}") -print(f"evaluations : {result.nfev}") -print("search slot management check passed.") -``` - ---- - -## Step 13 — Candidate Generation (`suggest_next_infill_point()`) - -```python -def _thread_search_task(): - with _surrogate_lock: - return self.suggest_next_infill_point() -``` - -`suggest_next_infill_point()` runs the acquisition function to identify the most -promising candidate for the next objective evaluation. -The acquisition strategy is controlled by `acquisition` (default `"y"`, minimising the -surrogate prediction directly). -The acquisition optimiser (default `"differential_evolution"`) searches the transformed, -reduced search space. -When the optimiser fails, the fallback strategy selected by -`acquisition_failure_strategy` applies; `"random"` (the default) draws a uniform random -point. -In the parallel path this method is always called inside `_thread_search_task()`, which -holds `_surrogate_lock` for the duration of the call, preventing simultaneous surrogate -access by multiple search threads or by the main-thread refit. - -```{python} -from spotoptim import SpotOptim -from spotoptim.function import sphere - -opt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], - n_initial=5, max_iter=10, acquisition="ei", seed=0, n_jobs=2) -result = opt.optimize() -assert opt.acquisition == "ei" -print(f"acquisition : {opt.acquisition}") -print(f"best : {result.fun:.6f}") -print("suggest_next_infill_point check passed.") -``` - ---- - -## Step 14 — Future Completion and Routing - -```python -done, _ = wait(futures.keys(), return_when=FIRST_COMPLETED) -for fut in done: - ftype = futures.pop(fut) - res = fut.result() - if ftype == "search": - x_cand = res - pending_cands.append(x_cand) - if _batch_ready(): - _flush_batch() - elif ftype == "batch_eval": - _future_n_pts.pop(fut, None) - X_done, y_done = res - # process batch... -``` - -`concurrent.futures.wait()` with `return_when=FIRST_COMPLETED` blocks until at least -one future finishes, then returns all futures that are done. -Each completed future is popped from `futures` and routed by its type tag. -For a `"search"` future the returned candidate is appended to `pending_cands`; if this -pushes the list over the batch threshold, `_flush_batch()` is called immediately to -avoid an unnecessary extra loop iteration. -For a `"batch_eval"` future the corresponding entry in `_future_n_pts` is removed so -that budget accounting is unblocked before the storage update begins. -If a result is an `Exception` — indicating a remote failure — the error is optionally -printed, the budget entry is removed, and the loop continues; failed search slots are -refilled in the next iteration, and failed eval points are lost without charging the -budget counter. - -```{python} -import numpy as np -from spotoptim import SpotOptim -from spotoptim.function import sphere - -opt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], - n_initial=5, max_iter=15, seed=0, n_jobs=2) -result = opt.optimize() -assert len(opt.y_) <= 15 -assert np.all(np.isfinite(opt.y_)) -print(f"evaluations stored : {len(opt.y_)}") -print("future routing check passed.") -``` - ---- - -## Step 15 — Batch Evaluation Processing (`update_success_rate()`, `_update_storage_steady()`, `fit_scheduler()`) - -```python -for xi, yi in zip(X_done, y_done): - self.update_success_rate(np.array([yi])) - self._update_storage_steady(xi, yi) - self.n_iter_ += 1 -with _surrogate_lock: - self.fit_scheduler() -``` - -When a batch evaluation completes successfully, the main thread processes every point in -the returned `(X_done, y_done)` pair before refitting the surrogate. -For each point, `update_success_rate()` records whether the new value improves on -`best_y_`, maintaining the rolling `success_rate` attribute. -`_update_storage_steady()` appends the point to `X_` and `y_`, updates `best_x_` and -`best_y_` if an improvement is found, and synchronises `min_y` and `min_X`. -`n_iter_` is incremented once per point so that it reflects the total number of -post-initial-design evaluations. -After all points in the batch have been stored, `fit_scheduler()` is called once under -`_surrogate_lock`, refitting the surrogate on the updated training window. -Batching the refit in this way — one call per batch rather than one call per point — -improves efficiency when `eval_batch_size > 1` and ensures that in-flight search -threads always read a self-consistent model. - -```{python} -import numpy as np -from spotoptim import SpotOptim -from spotoptim.function import sphere - -opt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], - n_initial=5, max_iter=15, seed=0, n_jobs=2) -opt.optimize() -print(f"success_rate : {opt.success_rate:.4f}") -print(f"X_ shape : {opt.X_.shape}") -print(f"y_ shape : {opt.y_.shape}") -assert 0.0 <= opt.success_rate <= 1.0 -assert opt.X_.shape[0] == opt.y_.shape[0] -print("batch processing check passed.") -``` - ---- - -## Step 16 — Termination and Result Assembly - -```python -return "FINISHED", OptimizeResult( - x=self.best_x_, - fun=self.best_y_, - nfev=len(self.y_), - nit=self.n_iter_, - success=True, - message="Optimization finished (Steady State)", - X=self.X_, - y=self.y_, -) -``` - -`optimize_steady_state()` exits the while loop when `len(y_) >= effective_max_iter` or -the wall-clock limit is exceeded. -It always returns `"FINISHED"` — there is no restart mechanism in the parallel path. -The `OptimizeResult` object carries the best solution (`x`, `fun`), total evaluation -count (`nfev`), iteration count (`nit`), the full evaluation history (`X`, `y`), and a -fixed termination message that identifies the result as coming from the steady-state -engine. -The `success` flag is always `True` because partial results are still valid whenever at -least one evaluation completed successfully; the guard at the end of Phase 1 ensures -the method does not return an empty result. - -```{python} -from spotoptim import SpotOptim -from spotoptim.function import sphere - -opt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], - n_initial=5, max_iter=10, seed=0, n_jobs=2) -result = opt.optimize() -first_line = result.message.splitlines()[0] -print(f"termination : {first_line}") -assert "Steady State" in result.message -assert result.success -print("termination check passed.") -``` - ---- - -## Complete Parallel Run Summary - -@tbl-par summarises every step executed along the parallel path in call order: - -| Step | Method / Phase | Purpose | -|-----:|----------------|---------| -| 1 | `execute_optimization_run()` | Dispatch to parallel path when `n_jobs > 1` | -| 2 | `optimize_steady_state()` | Parallel orchestrator; manages pools and phases | -| 3 | `set_seed()`, `get_initial_design()`, `curate_initial_design()` | Seed RNG, generate and curate initial design; pre-fill `y0_prefilled` for restart-injected point | -| 4 | `is_gil_disabled()` | Detect GIL state; select `ProcessPoolExecutor` or `ThreadPoolExecutor` for eval | -| 5 | Phase 1 submission | Store injected points directly; submit remaining `n_to_submit` points to `eval_pool` | -| 6 | `_update_storage_steady()` | Collect initial results; append each valid `(x, y)` to storage | -| 7 | `_init_tensorboard()`, `update_stats()`, `get_best_xy_initial_design()` | Log initial design; compute statistics; identify initial best | -| 8 | `fit_scheduler()` | First surrogate fit (no lock needed, no search threads active) | -| 9 | `optimize_steady_state()` while loop | Main loop: iterate until budget or time exhausted | -| 10 | `_batch_ready()` | Check whether `pending_cands` should be flushed | -| 11 | `_flush_batch()` | Dispatch all pending candidates as one batch eval to `eval_pool` | -| 12 | `_thread_search_task()` slot fill | Submit up to `n_jobs` search tasks under budget guard | -| 13 | `suggest_next_infill_point()` | Optimise acquisition under `_surrogate_lock` to propose candidate | -| 14 | `wait(FIRST_COMPLETED)` routing | Block until any future completes; route by `"search"` or `"batch_eval"` tag | -| 15 | `update_success_rate()`, `_update_storage_steady()`, `fit_scheduler()` | Process batch result; update storage and best; refit surrogate under lock | -| 16 | Return `"FINISHED"` + `OptimizeResult` | Assemble and return final result | - -: Complete Parallel Run Summary {#tbl-par} diff --git a/docs/optimize_seq.qmd b/docs/optimize_seq.qmd index fa0382f7..42e4ab1a 100644 --- a/docs/optimize_seq.qmd +++ b/docs/optimize_seq.qmd @@ -4,13 +4,13 @@ description: "Step-by-step walkthrough of execute_optimization_run() and every m --- This document traces every step executed by `SpotOptim.execute_optimization_run()` along -the sequential code path (`n_jobs=1`), in the order they occur. +the sequential optimization path, in the order they occur. Each section describes one method with a `{python}` code block that can be executed directly. The public entry point is `optimize()`, which manages the outer restart loop and delegates each cycle to `execute_optimization_run()`. -When `n_jobs == 1` (the default), that dispatcher routes to `optimize_sequential_run()`, -which coordinates initialisation, storage setup, and the main iteration loop. +That method routes to `optimize_sequential_run()`, which coordinates initialisation, +storage setup, and the main iteration loop. This document covers that path in full. Run all related tests with: @@ -23,21 +23,11 @@ uv run pytest tests/test_spotoptim_deep.py -v ## Step 1 — Dispatch (`execute_optimization_run()`) -```python -if self.n_jobs > 1: - return self.optimize_steady_state(...) -else: - return self.optimize_sequential_run(...) -``` - -`execute_optimization_run()` is the routing layer between the outer restart loop in -`optimize()` and the actual optimisation engine. -Its sole responsibility is to examine `n_jobs` and forward all arguments to either -`optimize_steady_state()` (parallel) or `optimize_sequential_run()` (sequential). -It returns a `(status, OptimizeResult)` tuple in both cases, which `optimize()` uses to -decide whether to restart or terminate. -The optional `shared_best_y` and `shared_lock` parameters support inter-worker -coordination in the parallel path; they are `None` in sequential mode. +`execute_optimization_run()` is the thin pass-through between the outer restart loop in +`optimize()` and the sequential optimisation engine. +It forwards all arguments to `optimize_sequential_run()` and returns a +`(status, OptimizeResult)` tuple, which `optimize()` uses to decide whether to restart +or terminate. ```{python} import time @@ -46,7 +36,7 @@ from spotoptim import SpotOptim from spotoptim.function import sphere opt = SpotOptim(fun=sphere, bounds=[(-5, 5), (-5, 5)], - n_initial=5, max_iter=10, seed=0, n_jobs=1) + n_initial=5, max_iter=10, seed=0) status, result = opt.execute_optimization_run(timeout_start=time.time()) print(f"status : {status}") print(f"best : {result.fun:.6f}") @@ -702,7 +692,7 @@ print("determine_termination check passed.") | Step | Method | Purpose | |-----:|--------|---------| -| 1 | `execute_optimization_run()` | Dispatch to sequential or parallel path | +| 1 | `execute_optimization_run()` | Entry point, delegates to optimize_sequential_run() | | 2 | `optimize_sequential_run()` | Sequential orchestrator | | 3 | `_initialize_run()` | Seed RNG, generate and evaluate initial design | | 4 | `rm_initial_design_NA_values()` | Remove NaN/inf from initial evaluations | diff --git a/docs/reference/index.qmd b/docs/reference/index.qmd index e5eede6d..f76c1bbd 100644 --- a/docs/reference/index.qmd +++ b/docs/reference/index.qmd @@ -122,7 +122,7 @@ | | | | --- | --- | -| [plot](plot.qmd#spotoptim.plot) | | +| [plot](plot.qmd#spotoptim.plot) | Plotting and visualization utilities for SpotOptim. | | [plot.contour](plot.contour.qmd#spotoptim.plot.contour) | | | [plot.contour.contourf_plot](plot.contour.contourf_plot.qmd#spotoptim.plot.contour.contourf_plot) | Creates contour plots (single or faceted) using matplotlib. | | [plot.contour.mo_generate_plot_grid](plot.contour.mo_generate_plot_grid.qmd#spotoptim.plot.contour.mo_generate_plot_grid) | Generate a grid of input variables and apply objective functions. | diff --git a/docs/slurm.qmd b/docs/slurm.qmd deleted file mode 100644 index 23c6c6ee..00000000 --- a/docs/slurm.qmd +++ /dev/null @@ -1,293 +0,0 @@ ---- -title: "Running spotoptim on the GWDG NHR Cluster (Slurm)" -description: "End-to-end recipe for running a parallel spotoptim experiment on the GWDG NHR login node glogin-p3 with 16 CPUs / n_jobs=16." -execute: - eval: false ---- - -This chapter shows how to run a parallel `spotoptim` optimization on the -[GWDG NHR](https://docs.hpc.gwdg.de/) cluster from the -`glogin-p3.hpc.gwdg.de` login node, using the `standard96s:shared` CPU -partition with 16 cores and `n_jobs=16`. - -The flow has three phases: - -1. **Locally** — build a `SpotOptim` instance, freeze it with - `save_experiment(...)`. The pickle holds the objective, bounds, surrogate, - `n_jobs`, seed, and everything else needed to resume. -2. **On the cluster** — `sbatch` a thin shell script that loads `uv`, then - calls `SpotOptim.load_experiment(...)`, runs `optimize()`, and writes the - result with `save_result(...)`. -3. **Locally again** — `scp` the result back and analyse it with - `SpotOptim.load_result(...)`. - -::: {.callout-note} -### What changed compared to the spotpython workflow - -The legacy `a_06_slurm.qmd` chapter (`spotpython`) needed two scripts on the -remote machine: `startSlurm.sh` plus a `startPython.py` wrapper that called -`load_and_run_spot_python_experiment(...)`. With `spotoptim` the wrapper is -unnecessary: `SpotOptim.load_experiment(...)` and `opt.optimize()` are the -public API, and parallelism is configured by setting `n_jobs` on the -`SpotOptim` constructor — there is no separate "control" object. -::: - -## Prerequisites - -* SSH access to `glogin-p3.hpc.gwdg.de` as your project user `uxxxxx` - (NHR account names follow the pattern `u` + five digits; your public key - registered via [id.academiccloud.de](https://id.academiccloud.de/) → - Security → SSH Public Keys). The login pattern follows the standard GWDG - documentation — see - [docs.hpc.gwdg.de/start_here/connecting](https://docs.hpc.gwdg.de/start_here/connecting/index.html). -* A `~/.ssh/config` host alias makes the rest of the chapter copy-pastable: - -```bash -# ~/.ssh/config (local machine) -Host glogin-p3 - Hostname glogin-p3.hpc.gwdg.de - User uxxxxx -``` - -## One-time cluster setup - -Log in once and clone the `spotoptim` repository under `$HOME/workspace`. The -GWDG environment provides `uv` as a module, so no `conda` step is needed. - -```bash -ssh glogin-p3 -mkdir -p ~/workspace && cd ~/workspace -git clone https://github.com/sequential-parameter-optimization/spotoptim.git -cd spotoptim - -# Compute nodes need an explicit proxy when downloading dependencies. -export http_proxy=http://www-cache.gwdg.de:3128 -export https_proxy=http://www-cache.gwdg.de:3128 - -module purge -module load gcc uv -uv python pin 3.13 -uv sync # creates .venv/, installs spotoptim editable -uv run python -c "from spotoptim import SpotOptim; print('ok')" -``` - -After `uv sync` succeeds, `~/workspace/spotoptim/.venv/` is the environment -the Slurm script will activate via `uv run`. There is no per-job environment -setup — the lock file makes resync only-on-change. - -## Build the experiment locally - -Create a `SpotOptim` instance with `n_jobs=16` and freeze it. The example -uses the built-in 3-D `sphere` test function so that the chapter is -reproducible without external data: - -```{python} -from spotoptim import SpotOptim -from spotoptim.function import sphere - -PREFIX = "a06" - -opt = SpotOptim( - fun=sphere, - bounds=[(-5.0, 5.0)] * 3, - n_initial=16, # one batch fills all 16 workers in parallel - max_iter=80, # total evaluation budget (incl. the initial design) - n_jobs=16, # process pool size on the compute node - eval_batch_size=1, # set > 1 if the objective accepts a batch - seed=0, - verbose=True, -) - -opt.save_experiment(prefix=PREFIX, path=".") -# → writes ./a06_exp.pkl -``` - -`save_experiment` uses `dill` so that closures and lambdas survive the -round-trip. The output file is named `_exp.pkl`. Replace the body of -`fun=...` with any picklable callable to plug your own problem in. - -::: {.callout-tip} -### `n_jobs` and `eval_batch_size` - -`n_jobs > 1` activates `optimize_steady_state()` — see -[Parallel Optimization](optimize_parallel.qmd) for the full data flow. Use -`-1` to mean "all CPU cores on the worker node". `eval_batch_size` collects -that many candidate points before a single dispatch to the pool, which is -worth setting only when your objective natively handles batched input. -::: - -## Copy the experiment to the cluster - -```bash -ssh glogin-p3 'mkdir -p ~/runs/spotoptim/logs' -scp a06_exp.pkl glogin-p3:~/runs/spotoptim/ -``` - -`~/runs/spotoptim/` is a convention used in this chapter; pick any -directory — just keep `logs/` as a sub-directory because the Slurm script -writes its `.out`/`.err` files there. - -## The Slurm submission script - -The repository ships a reference batch script at -`scripts/slurm/run_spotoptim.sh`. Inline: - -```bash -#!/bin/bash -#SBATCH --job-name=spotoptim -#SBATCH --partition=standard96s:shared -#SBATCH --cpus-per-task=16 -#SBATCH --mem=16G -#SBATCH --time=24:00:00 -#SBATCH --output=logs/spotoptim_%j.out -#SBATCH --error=logs/spotoptim_%j.err -#SBATCH --constraint=inet - -set -euo pipefail -EXP_PKL="$1" - -# GWDG proxy + thread pinning (one BLAS thread per worker process). -export http_proxy=http://www-cache.gwdg.de:3128 -export https_proxy=http://www-cache.gwdg.de:3128 -export OMP_NUM_THREADS=1 -export OPENBLAS_NUM_THREADS=1 -export MKL_NUM_THREADS=1 -export PYTHONUNBUFFERED=1 - -mkdir -p logs -module purge 2>/dev/null || true -module load gcc uv - -cd "${SPOTOPTIM_REPO:-$HOME/workspace/spotoptim}" -uv run python scripts/slurm/run_spotoptim.py "$EXP_PKL" -``` - -::: {.callout-warning} -### Why `OMP_NUM_THREADS=1` is mandatory - -The 16 worker processes inherit `OMP_NUM_THREADS` from the batch -environment. Without pinning, each worker would launch its own BLAS -thread-pool of `cpu_count()` threads, leading to 16 × 16 = 256 threads on a -shared node and severe contention. The graph-elites benchmark in -`~/workspace/graph-elites/gwdg/slurm/run_spot_monet.sh` uses the same -pinning for the same reason. -::: - -The Python runner `scripts/slurm/run_spotoptim.py` is a 30-line wrapper: - -```python -import argparse -from pathlib import Path -from spotoptim import SpotOptim - -p = argparse.ArgumentParser() -p.add_argument("exp_pkl", type=Path) -args = p.parse_args() - -exp_path = args.exp_pkl.resolve() -prefix = exp_path.name.removesuffix("_exp.pkl") - -opt = SpotOptim.load_experiment(str(exp_path)) -result = opt.optimize() -opt.save_result(prefix=prefix, path=str(exp_path.parent)) - -print(f"nfev={result.nfev} fun={result.fun:.6g} x={result.x}") -``` - -`optimize()` honours the `n_jobs` value baked into the experiment, so the -runner itself never mentions parallelism — it does load → run → save. - -## Submit the job - -```bash -ssh glogin-p3 -cd ~/runs/spotoptim -sbatch ~/workspace/spotoptim/scripts/slurm/run_spotoptim.sh \ - ~/runs/spotoptim/a06_exp.pkl -# → Submitted batch job 12345678 -``` - -Pass the experiment path as an absolute path; the Slurm script `cd`s into the -spotoptim repo, so a relative path would resolve there instead of in -`~/runs/spotoptim/`. - -::: {.callout-tip} -### Faster scheduling for small jobs - -Add `--qos=2h` to the `sbatch` call when your run fits in 2 hours; the -high-priority QoS usually starts within minutes but rejects walltime > 2 h. -Override the time at submit-time, not in the script header: - -```bash -sbatch --qos=2h --time=00:30:00 \ - ~/workspace/spotoptim/scripts/slurm/run_spotoptim.sh ... -``` -::: - -## Monitor the job - -```bash -squeue --me -sacct -j --format=JobID,State,Elapsed,MaxRSS,ExitCode -tail -f ~/runs/spotoptim/logs/spotoptim_.out -``` - -A successful run prints, near the end of the `.out` file: - -``` -=== spotoptim job === -Job ID : 12345678 -CPUs : 16 -Mem : 16384 MB -… -nfev=80 fun=0.000123 x=[ 0.0089 -0.0083 0.0027] -=== Job completed at … === -``` - -If you see `OUT_OF_MEMORY` from `sacct`, raise `--mem` (the budget should be -roughly `n_jobs × 1 GB`; spotoptim's surrogate adds a small constant on top). - -## Copy the result back and analyse - -```bash -scp glogin-p3:~/runs/spotoptim/a06_res.pkl . -``` - -```{python} -from spotoptim import SpotOptim - -opt = SpotOptim.load_result("a06_res.pkl") - -print("best fun :", opt.best_y_) -print("best x :", opt.best_x_) -print("nfev :", opt.X_.shape[0]) - -opt.plot_progress(log_y=True) -``` - -`load_result` reinitialises the surrogate and the LHS sampler that were -stripped before pickling, so all the analysis methods on `SpotOptim` -(`plot_progress`, `print_results`, `get_importance`, …) work as if the -experiment had been run locally. - -## Slurm command reference - -| Command | Description | -| :-- | :-- | -| `sbatch run_spotoptim.sh _exp.pkl` | Submit a job that runs `optimize()` on the supplied pickle. | -| `sbatch --qos=2h --time=02:00:00 …` | High-priority QoS; faster scheduling, max walltime 2 h. | -| `squeue --me` | List your queued and running jobs. | -| `sacct -j --format=JobID,State,Elapsed,MaxRSS,ExitCode` | Per-job accounting (use `MaxRSS` to right-size `--mem`). | -| `scancel ` | Cancel a job. | -| `sinfo -p standard96s:shared` | Node availability on the CPU shared partition. | -| `module load gcc uv` | Load `gcc` (often a `uv` dependency) plus the `uv` module on the login or compute node. | -| `scp file glogin-p3:~/runs/spotoptim/` | Copy a file to the cluster. | -| `scp glogin-p3:~/runs/spotoptim/_res.pkl .` | Copy the result back. | -| `show-quota` | Show your storage quotas (HOME, project, workspaces). | - -## See also - -* [Parallel Optimization](optimize_parallel.qmd) — internal control flow when - `n_jobs > 1`. -* [GWDG HPC documentation](https://docs.hpc.gwdg.de/) — partitions, QoS, - module system, GPU partitions (`grete`). diff --git a/docs/spotoptim_class.qmd b/docs/spotoptim_class.qmd index 8164cafa..93d6a2aa 100644 --- a/docs/spotoptim_class.qmd +++ b/docs/spotoptim_class.qmd @@ -94,12 +94,6 @@ description: "Structure of the Methods" -### TASK_OPTIM_PARALLEL: - -* _update_storage_steady() -* optimize_steady_state() - - ### TASK_MO: * store_mo() @@ -149,7 +143,7 @@ description: "Structure of the Methods" ## The Surrogate-model-based Optimization Process -In the following, we will consider the `optimize` method as the main entry point for the surrogate-model-based optimization process. `optimize` calls the dispatcher for the sequential and the parallel (steady-state) optimization runs, which are the main optimization loops. We consider the sequential run, which is started via the method `optimize_sequential_run`. We list all the calls to `self.*` methods that are made in the sequential optimization loop and enumerate the dependencies. Every time, a self-method is called, the numbering goes down by one level. If the method is finished, we go back up one level. +In the following, we will consider the `optimize` method as the main entry point for the surrogate-model-based optimization process. `optimize` runs the sequential optimization loop, which is started via the method `optimize_sequential_run`. We list all the calls to `self.*` methods that are made in the sequential optimization loop and enumerate the dependencies. Every time, a self-method is called, the numbering goes down by one level. If the method is finished, we go back up one level. This results in a tree-like structure of method calls, which we will use to improve (refactor) the structure of the code in the `SpotOptim` class in @SpotOptim.py. 1. _initialize_run() diff --git a/docs/spotoptim_init.qmd b/docs/spotoptim_init.qmd index 48ad47f5..4c46fa1c 100644 --- a/docs/spotoptim_init.qmd +++ b/docs/spotoptim_init.qmd @@ -148,28 +148,17 @@ print("Parameter inference check passed.") ```python if max_iter < n_initial: raise ValueError(...) -if n_jobs == -1: - n_jobs = os.cpu_count() or 1 -elif n_jobs == 0 or n_jobs < -1: - raise ValueError(...) -if eval_batch_size < 1: - raise ValueError(...) if acquisition_optimizer_kwargs is None: acquisition_optimizer_kwargs = {"maxiter": 10000, "gtol": 1e-9} ``` -Four checks run before the configuration object is assembled. `max_iter` +Two checks run before the configuration object is assembled. `max_iter` must be at least `n_initial` because the total budget must accommodate the -initial design. `n_jobs` follows the scikit-learn convention: `-1` is -resolved to `os.cpu_count()`, while `0` and values below `-1` are rejected. -`eval_batch_size` controls how many candidate points accumulate before a -single vectorised call is dispatched to the process pool and must be at -least `1`. Finally, when `acquisition_optimizer_kwargs` is not supplied it +initial design. When `acquisition_optimizer_kwargs` is not supplied it is initialised to `{"maxiter": 10000, "gtol": 1e-9}`, providing tight convergence tolerances for the default differential-evolution run. ```{python} -import os from spotoptim import SpotOptim from spotoptim.function import sphere @@ -184,18 +173,6 @@ try: except ValueError as e: print(f"Caught expected error: {e}") -# n_jobs=-1 resolves to all available CPU cores -opt_p = SpotOptim(fun=sphere, bounds=[(-5, 5)], n_jobs=-1) -assert opt_p.n_jobs == (os.cpu_count() or 1) -print(f"n_jobs=-1 resolved to: {opt_p.n_jobs}") - -# n_jobs=0 is rejected -try: - SpotOptim(fun=sphere, bounds=[(-5, 5)], n_jobs=0) - raise AssertionError("Expected ValueError was not raised") -except ValueError as e: - print(f"Caught expected error: {e}") - # acquisition_optimizer_kwargs default opt_aq = SpotOptim(fun=sphere, bounds=[(-5, 5)]) assert opt_aq.acquisition_optimizer_kwargs == {"maxiter": 10000, "gtol": 1e-9} @@ -893,7 +870,7 @@ print("TensorBoard no-op check passed.") | 2 | Machine epsilon | `self.eps` | | 3 | `tolerance_x` default | `tolerance_x` | | 4 | Parameter inference from `fun` | `bounds`, `var_type`, `var_name`, `var_trans` | -| 5 | Validate `max_iter`, `n_jobs`, `eval_batch_size`; default `acquisition_optimizer_kwargs` | — | +| 5 | Validate `max_iter`; default `acquisition_optimizer_kwargs` | — | | 6 | Create `SpotOptimConfig` | `self.config` | | 7 | Create `SpotOptimState` | `self.state` | | 8 | Store callable and `objective_names` | `self.fun`, `self.objective_names` | diff --git a/docs/user_guide.qmd b/docs/user_guide.qmd index 443f8812..6339f748 100644 --- a/docs/user_guide.qmd +++ b/docs/user_guide.qmd @@ -72,7 +72,7 @@ then explore the modules that match your use case. ### Utilities -- [Utilities](user_guide/utils.qmd) --- Boundaries, transforms, PCA, OCBA, parallel helpers +- [Utilities](user_guide/utils.qmd) --- Boundaries, transforms, PCA, OCBA, TensorBoard logging - [Neural Networks](user_guide/nn.qmd) --- PyTorch MLP and LinearRegressor for building objectives - [Datasets](user_guide/data.qmd) --- DiabetesDataset and data loaders for PyTorch workflows @@ -93,14 +93,14 @@ then explore the modules that match your use case. src/spotoptim/ ├── SpotOptim.py # Core optimizer ├── core/ # Protocol, storage, experiment control -├── optimizer/ # Acquisition, steady-state, scipy wrapper +├── optimizer/ # Acquisition, scipy wrapper ├── surrogate/ # Kriging, MLP surrogate, Nystroem, sklearn pipeline ├── nn/ # PyTorch MLP, LinearRegressor ├── function/ # Objective functions (single/multi-objective, remote, torch) ├── sampling/ # LHS, Sobol, grid, clustered designs ├── reporting/ # Results extraction, analysis utilities ├── plot/ # Surrogate visualization, contour, multi-objective -├── utils/ # Boundaries, transforms, PCA, OCBA, TensorBoard, parallel +├── utils/ # Boundaries, transforms, PCA, OCBA, TensorBoard ├── mo/ # Multi-objective: Morris-Mitchell, Pareto front ├── hyperparameters/ # Parameter set management for NN tuning ├── data/ # Dataset loaders (e.g., DiabetesDataset) diff --git a/docs/user_guide/core.qmd b/docs/user_guide/core.qmd index 07674940..bc910e4c 100644 --- a/docs/user_guide/core.qmd +++ b/docs/user_guide/core.qmd @@ -15,8 +15,7 @@ extending the package or building custom workflows. `SpotOptimProtocol` is a structural typing protocol (PEP 544) that defines the interface extracted modules expect. Instead of importing the concrete `SpotOptim` class (which would create circular imports), -modules like `optimizer.steady_state` and `reporting.analysis` accept -any object matching this protocol. +modules like `reporting.analysis` accept any object matching this protocol. The protocol declares configuration attributes (`bounds`, `max_iter`, `acquisition`, `surrogate`, etc.) and mutable state attributes (`X_`, diff --git a/docs/user_guide/spotoptim.qmd b/docs/user_guide/spotoptim.qmd index 165bdf12..9a3d106d 100644 --- a/docs/user_guide/spotoptim.qmd +++ b/docs/user_guide/spotoptim.qmd @@ -324,32 +324,6 @@ is injected into the new initial design after each restart. --- -## Parallel Evaluation - -Set `n_jobs` to evaluate multiple points in parallel. Use `n_jobs=-1` -to use all available CPU cores: - -```{python} -from spotoptim import SpotOptim -from spotoptim.function import sphere - -opt = SpotOptim( - fun=sphere, - bounds=[(-5, 5), (-5, 5)], - n_jobs=2, - eval_batch_size=2, - max_iter=20, - n_initial=10, - seed=0, -) -result = opt.optimize() - -print(f"Best f(x) : {result.fun:.6f}") -print(f"Evaluations: {result.nfev}") -``` - ---- - ## Configuration Access All constructor parameters are stored in `opt.config` (a `SpotOptimConfig` diff --git a/docs/user_guide/utils.qmd b/docs/user_guide/utils.qmd index 26469e90..fd18a2e1 100644 --- a/docs/user_guide/utils.qmd +++ b/docs/user_guide/utils.qmd @@ -1,6 +1,6 @@ --- title: "Utilities" -description: "Boundaries, transformations, PCA, OCBA, scaling, and parallel helpers." +description: "Boundaries, transformations, PCA, OCBA, and scaling helpers." --- spotoptim ships a collection of utility functions that support the @@ -173,19 +173,3 @@ print(f"Shape: {X_scaled.shape}") print(f"Mean after scaling: {X_scaled.mean(dim=0)}") ``` ---- - -## Parallel Evaluation - -`is_gil_disabled` checks whether the current Python interpreter is a -free-threaded build (PEP 703). On standard CPython the GIL is enabled -and this returns `False`. spotoptim uses this check internally to decide -whether thread-based parallelism is safe for objective evaluation. - -```{python} -from spotoptim.utils import is_gil_disabled - -result = is_gil_disabled() -print(f"GIL disabled: {result}") -print(f"Return type: {type(result).__name__}") -``` diff --git a/pyproject.toml b/pyproject.toml index a7c87e9b..a6a085a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,27 +17,15 @@ dependencies = [ "numpy>=1.24.3", "scipy>=1.10.1", "scikit-learn>=1.5.0", - # MkDocs dependencies removed — documentation now uses Quarto + quartodoc. - # Install doc-build tools via: pip install spotoptim[docs] - # or: pip install -r requirements-docs.txt - "jupyter>=1.1.1", - "matplotlib>=3.10.7", "pandas>=2.1.0", - "requests>=2.32.3", - "torch>=2.9.1", - "tensorboard>=2.20.0", "tabulate>=0.9.0", - "xgboost>=3.1.1", - "seaborn>=0.13.2", - "spotdesirability>=0.0.1", - "statsmodels>=0.14.6", "dill>=0.4.1", - "flake8>=7.3.0", - "black>=26.1.0", - "importlib-metadata>=8.7.1", - "ty>=0.0.29", ] +# Default-installed dev tooling (plain `uv sync`). The canonical dev command +# is `uv sync --extra dev`, whose `dev` extra additionally pulls the runtime +# extras (`spotoptim[all]`) and `spotdesirability` needed to run the full +# test suite — those cannot be expressed in a PEP 735 group. [dependency-groups] dev = [ "pytest>=7.4.0", @@ -50,9 +38,27 @@ dev = [ "safety>=3.0.0", "bandit>=1.8.0", "pre-commit>=3.7.0", + "ty>=0.0.29", ] [project.optional-dependencies] +torch = [ + "torch>=2.9.1", + "tensorboard>=2.20.0", +] +viz = [ + "matplotlib>=3.10.7", + "seaborn>=0.13.2", +] +stats = [ + "statsmodels>=0.14.6", +] +remote = [ + "requests>=2.32.3", +] +all = [ + "spotoptim[torch,viz,stats,remote]", +] dev = [ "pytest>=7.4.0", "pytest-cov>=4.1.0", @@ -64,6 +70,9 @@ dev = [ "safety>=3.0.0", "bandit>=1.8.0", "pre-commit>=3.7.0", + "ty>=0.0.29", + "spotoptim[all]", + "spotdesirability>=0.0.1", ] docs = [ # Quarto API documentation generator (replaces mkdocstrings) @@ -72,9 +81,9 @@ docs = [ "griffe>=1.7.3", # Quarto Jupyter integration "jupyter>=1.1.1", - # Transitive dependencies for griffe CLI + # Transitive dependencies for griffe CLI "colorama>=0.4.6", - "importlib_metadata" + "importlib_metadata", ] [tool.pytest.ini_options] @@ -116,3 +125,31 @@ packages = ["src/spotoptim"] [tool.hatch.build] include = ["src/spotoptim/datasets/*.csv"] + +# ── Tooling baseline (roadmap #6): converge on the spot* standard ──────────── +# Ruff replaces the removed legacy .flake8. Line length matches black; CI runs +# `ruff check` non-blocking. +[tool.ruff] +line-length = 88 +target-version = "py313" + +[tool.black] +target-version = ["py313"] + +[tool.isort] +profile = "black" + +[tool.coverage.run] +branch = true +source = ["src/spotoptim"] + +[tool.coverage.report] +# Ratchet: lock in the current ~71% fast-suite coverage so it cannot regress; +# 68 leaves margin for platform variance. Raise deliberately as coverage +# improves. The fast CI gate runs pytest with --cov, enforcing this. +fail_under = 68 + +# Astral `ty` type checker. Run non-blocking in CI (continue-on-error) so it +# surfaces type debt without failing the build; tighten to a gate later. +[tool.ty.environment] +python-version = "3.13" diff --git a/src/spotoptim/SpotOptim.py b/src/spotoptim/SpotOptim.py index 29d17e4c..a69963a7 100644 --- a/src/spotoptim/SpotOptim.py +++ b/src/spotoptim/SpotOptim.py @@ -4,7 +4,6 @@ import numpy as np import random -import torch from functools import partial from dataclasses import dataclass, field from typing import Callable, Optional, Tuple, List, Any, Dict, Union, Literal @@ -15,19 +14,9 @@ from sklearn.gaussian_process import GaussianProcessRegressor from sklearn.gaussian_process.kernels import Matern, ConstantKernel import warnings -import matplotlib.pyplot as plt from numpy import append import time -import os from sklearn.cluster import KMeans -from spotoptim.plot.visualization import ( - plot_surrogate, - plot_progress, - plot_important_hyperparameter_contour, - _plot_surrogate_with_factors, - _generate_mesh_grid, - _generate_mesh_grid_with_factors, -) from spotoptim.utils.convert import safe_float from spotoptim.utils import tensorboard as _tb from spotoptim.utils import ocba as _ocba @@ -39,7 +28,6 @@ from spotoptim.utils import dimreduction as _dimred from spotoptim.optimizer import acquisition as _acq from spotoptim.core import storage as _storage -from spotoptim.optimizer import steady_state as _steady from spotoptim.optimizer.wrapper import gpr_minimize_wrapper @@ -90,26 +78,6 @@ class SpotOptimConfig: window_size (Optional[int]): Size of the window for tricands. min_tol_metric (str): Metric for minimum tolerance. prob_surrogate (Optional[List[float]]): Probability of using surrogate. - n_jobs (int): Number of parallel workers. ``1`` runs sequentially. - Values ``> 1`` activate steady-state parallel optimization. - On standard GIL builds a hybrid executor is used: - ``ProcessPoolExecutor`` for objective evaluations (process - isolation; supports lambdas and closures via ``dill``) and - ``ThreadPoolExecutor`` for surrogate search tasks (shared heap; - zero serialization overhead). - On free-threaded Python builds (``python3.13t``, - ``--disable-gil``), both pools are ``ThreadPoolExecutor`` - instances, achieving true CPU-level parallelism without ``dill`` - for eval tasks. - ``-1`` resolves to ``os.cpu_count()`` (all available CPU cores). - ``0`` and values ``< -1`` raise ``ValueError``. - Defaults to ``1``. - eval_batch_size (int): Number of candidate points to accumulate before - dispatching a single ``fun(X_batch)`` call to the process pool. - ``1`` (default) dispatches each candidate immediately, preserving - current behavior. Values ``> 1`` reduce process-spawn and IPC - overhead when ``fun`` supports vectorized batch input. - Must be ``>= 1``. Defaults to ``1``. acquisition_optimizer_kwargs (Optional[Dict[str, Any]]): Keyword arguments for the acquisition function optimizer. args (Tuple): Arguments for the objective function. kwargs (Optional[Dict[str, Any]]): Keyword arguments for the objective function. @@ -165,7 +133,6 @@ class SpotOptimConfig: window_size=None, min_tol_metric="chebyshev", prob_surrogate=None, - n_jobs=1, acquisition_optimizer_kwargs=None, args=(), kwargs=None, @@ -211,8 +178,6 @@ class SpotOptimConfig: window_size: Optional[int] = None min_tol_metric: str = "chebyshev" prob_surrogate: Optional[List[float]] = None - n_jobs: int = 1 - eval_batch_size: int = 1 acquisition_optimizer_kwargs: Optional[Dict[str, Any]] = None args: Tuple = () kwargs: Optional[Dict[str, Any]] = None @@ -450,20 +415,6 @@ class SpotOptim(BaseEstimator): prob_de_tricands (float, optional): Probability of using differential evolution as an optimizer on the surrogate model. 1 - prob_de_tricands is the probability of using tricands. Defaults to 0.8. - n_jobs (int, optional): - Number of parallel workers. ``1`` (default) runs sequentially. - Values ``> 1`` activate steady-state parallel optimization: - objective evaluations and acquisition searches are dispatched - across ``n_jobs`` processes. Pass ``-1`` to use all available - CPU cores (``os.cpu_count()``). ``0`` and values ``< -1`` raise - ``ValueError``. Defaults to ``1``. - eval_batch_size (int, optional): - Number of candidate points gathered from search tasks before a - single ``fun(X_batch)`` call is dispatched to the process pool. - ``1`` (default) preserves one-point-per-call behavior. - Set to ``n_jobs`` or higher to exploit vectorized objective - functions and reduce process-spawn overhead. Ignored when - ``n_jobs == 1``. Must be ``>= 1``. Defaults to ``1``. window_size (int, optional): Window size for success rate calculation. min_tol_metric (str, optional): @@ -799,8 +750,6 @@ def __init__( window_size: Optional[int] = None, min_tol_metric: str = "chebyshev", prob_surrogate: Optional[List[float]] = None, - n_jobs: int = 1, - eval_batch_size: int = 1, acquisition_optimizer_kwargs: Optional[Dict[str, Any]] = None, args: Tuple = (), kwargs: Optional[Dict[str, Any]] = None, @@ -836,18 +785,6 @@ def __init__( f"max_iter represents the total function evaluation budget including initial design." ) - # Resolve n_jobs: -1 means "use all available CPU cores" (scikit-learn convention). - if n_jobs == -1: - n_jobs = os.cpu_count() or 1 - elif n_jobs == 0 or n_jobs < -1: - raise ValueError( - f"n_jobs must be a positive integer or -1 (all CPU cores), got {n_jobs}." - ) - - # Validate eval_batch_size. - if eval_batch_size < 1: - raise ValueError(f"eval_batch_size must be >= 1, got {eval_batch_size}.") - if acquisition_optimizer_kwargs is None: acquisition_optimizer_kwargs = {"maxiter": 10000, "gtol": 1e-9} @@ -891,8 +828,6 @@ def __init__( window_size=window_size, min_tol_metric=min_tol_metric, prob_surrogate=prob_surrogate, - n_jobs=n_jobs, - eval_batch_size=eval_batch_size, acquisition_optimizer_kwargs=acquisition_optimizer_kwargs, args=args, kwargs=kwargs, @@ -1067,10 +1002,16 @@ def set_seed(self) -> None: if self.seed is not None: random.seed(self.seed) np.random.seed(self.seed) - torch.manual_seed(self.seed) - if torch.cuda.is_available(): - torch.cuda.manual_seed(self.seed) - torch.cuda.manual_seed_all(self.seed) + # Only seed torch if it is already loaded (avoids eager-importing it + # when the caller is using a non-PyTorch surrogate). + import sys as _sys + + if "torch" in _sys.modules: + _torch = _sys.modules["torch"] + _torch.manual_seed(self.seed) + if _torch.cuda.is_available(): + _torch.cuda.manual_seed(self.seed) + _torch.cuda.manual_seed_all(self.seed) # ==================== # TASK_VARS: @@ -2545,8 +2486,7 @@ def optimize(self, X0: Optional[np.ndarray] = None) -> OptimizeResult: print("Global budget exhausted. Stopping restarts.") break - # Execute one optimization run using the remaining budget; dispatcher - # selects sequential vs parallel based on `n_jobs` and returns status/result. + # Execute one sequential optimization run using the remaining budget. status, result = self.execute_optimization_run( timeout_start, current_X0, @@ -2604,9 +2544,8 @@ def optimize(self, X0: Optional[np.ndarray] = None) -> OptimizeResult: f"Restart injection: Using best found point so far as starting point (f(x)={best_res.fun:.6f})." ) - if self.seed is not None and self.n_jobs == 1: - # In sequential mode we advance the seed between restarts to diversify the LHS. - # Parallel mode increments seeds per worker during dispatch. + if self.seed is not None: + # Advance the seed between restarts to diversify the LHS. self.seed += 1 # Continue loop else: @@ -2620,7 +2559,7 @@ def optimize(self, X0: Optional[np.ndarray] = None) -> OptimizeResult: # Find best result based on 'fun' best_result = min(self.restarts_results_, key=lambda r: r.fun) - # Merge results from all parallel runs (and sequential runs if any) + # Merge results from all restart runs X_all_list = [res.X for res in self.restarts_results_] y_all_list = [res.y for res in self.restarts_results_] @@ -2658,19 +2597,15 @@ def execute_optimization_run( X0: Optional[np.ndarray] = None, y0_known: Optional[float] = None, max_iter_override: Optional[int] = None, - shared_best_y=None, # New arg - shared_lock=None, # New arg ) -> Tuple[str, OptimizeResult]: - """Dispatcher for optimization run (Sequential vs Steady-State Parallel). - Depending on n_jobs, calls optimize_steady_state (n_jobs > 1) or optimize_sequential_run (n_jobs == 1). + """Entry point for a single sequential optimization run. + Delegates to optimize_sequential_run with the provided arguments. Args: timeout_start (float): Start time for timeout. X0 (Optional[np.ndarray]): Initial design points in Natural Space, shape (n_initial, n_features). y0_known (Optional[float]): Known best value for initial design. max_iter_override (Optional[int]): Override for maximum number of iterations. - shared_best_y (Optional[float]): Shared best value for parallel runs. - shared_lock (Optional[Lock]): Shared lock for parallel runs. Returns: Tuple[str, OptimizeResult]: Tuple containing status and optimization result. @@ -2687,7 +2622,6 @@ def execute_optimization_run( n_initial=5, max_iter=10, seed=0, - n_jobs=1, # Use sequential optimization for deterministic output verbose=True ) status, result = opt.execute_optimization_run(timeout_start=time.time()) @@ -2695,24 +2629,12 @@ def execute_optimization_run( print(result.message.splitlines()[0]) ``` """ - - # Dispatch to steady-state optimizer if proper parallelization is requested - if self.n_jobs > 1: - return self.optimize_steady_state( - timeout_start, - X0, - y0_known=y0_known, - max_iter_override=max_iter_override, - ) - else: - return self.optimize_sequential_run( - timeout_start, - X0, - y0_known=y0_known, - max_iter_override=max_iter_override, - shared_best_y=shared_best_y, - shared_lock=shared_lock, - ) + return self.optimize_sequential_run( + timeout_start, + X0, + y0_known=y0_known, + max_iter_override=max_iter_override, + ) def evaluate_function(self, X: np.ndarray) -> np.ndarray: """Evaluate objective function at points X. @@ -3134,8 +3056,6 @@ def optimize_sequential_run( X0: Optional[np.ndarray] = None, y0_known: Optional[float] = None, max_iter_override: Optional[int] = None, - shared_best_y=None, - shared_lock=None, ) -> Tuple[str, OptimizeResult]: """Perform a single sequential optimization run. Calls _initialize_run, rm_initial_design_NA_values, check_size_initial_design, init_storage, get_best_xy_initial_design, and _run_sequential_loop. @@ -3145,8 +3065,6 @@ def optimize_sequential_run( X0 (Optional[np.ndarray]): Initial design points in Natural Space, shape (n_initial, n_features). y0_known (Optional[float]): Known best value for initial design. max_iter_override (Optional[int]): Override for maximum number of iterations. - shared_best_y (Optional[float]): Shared best value for parallel runs. - shared_lock (Optional[Lock]): Shared lock for parallel runs. Returns: Tuple[str, OptimizeResult]: Tuple containing status and optimization result. @@ -3165,7 +3083,6 @@ def optimize_sequential_run( n_initial=5, max_iter=10, seed=0, - n_jobs=1, # Use sequential optimization for deterministic output verbose=True ) status, result = opt.optimize_sequential_run(timeout_start=time.time()) @@ -3174,10 +3091,6 @@ def optimize_sequential_run( ``` """ - # Store shared variable if provided - self.shared_best_y = shared_best_y - self.shared_lock = shared_lock - # Initialize: Set seed, Design, Evaluate Initial Design, Init Storage & TensorBoard X0, y0 = self._initialize_run(X0, y0_known) @@ -3455,7 +3368,6 @@ def _run_sequential_loop( ... n_initial=5, ... max_iter=10, ... seed=0, - ... n_jobs=1, # Use sequential optimization for deterministic output ... verbose=True ... ) >>> X0, y0 = opt._initialize_run(X0=None, y0_known=None) @@ -3861,25 +3773,6 @@ def _update_best_main_loop( Iteration 1: New best f(x) = 0.500000, mean best: f(x) = 1.500000 """ # Update best - # Determine global best value for printing if shared variable exists - global_best_val = None - if hasattr(self, "shared_best_y") and self.shared_best_y is not None: - # Sync with global shared value - lock_obj = getattr(self, "shared_lock", None) - if lock_obj is not None: - with lock_obj: - if ( - self.best_y_ is not None - and self.best_y_ < self.shared_best_y.value - ): - self.shared_best_y.value = self.best_y_ - - min_y_next = np.min(y_next) - if min_y_next < self.shared_best_y.value: - self.shared_best_y.value = min_y_next - - global_best_val = self.shared_best_y.value - current_best = np.min(y_next) if current_best < self.best_y_: best_idx_in_new = np.argmin(y_next) @@ -3904,8 +3797,6 @@ def _update_best_main_loop( progress_str = f"Evals: {progress:.1f}%" msg = f"Iter {self.n_iter_}" - if global_best_val is not None: - msg += f" | GlobalBest: {global_best_val:.6f}" msg += f" | Best: {self.best_y_:.6f} | Rate: {self.success_rate:.2f} | {progress_str}" if (self.repeats_initial > 1) or (self.repeats_surrogate > 1): @@ -3923,8 +3814,6 @@ def _update_best_main_loop( current_val = np.min(y_next) msg = f"Iter {self.n_iter_}" - if global_best_val is not None: - msg += f" | GlobalBest: {global_best_val:.6f}" msg += f" | Best: {self.best_y_:.6f} | Curr: {current_val:.6f} | Rate: {self.success_rate:.2f} | {progress_str}" if (self.repeats_initial > 1) or (self.repeats_surrogate > 1): @@ -4093,146 +3982,6 @@ def update_repeats_infill_points(self, x_next: np.ndarray) -> np.ndarray: x_next_repeated = x_next return x_next_repeated - # ==================== - # TASK_OPTIM_PARALLEL: - # * _update_storage_steady() - # * optimize_steady_state() - # ==================== - - def _update_storage_steady(self, x, y): - """Helper to safely append single point (for steady state). - This method is designed for the steady-state parallel optimization scenario, where new points are evaluated and returned asynchronously. - It safely appends new points to the existing storage of evaluated points and their function values, - while also updating the current best solution if the new point is better. - - Args: - x (ndarray): - New point(s) in original scale, shape (n_features,) or (N, n_features). - y (float or ndarray): - Corresponding function value(s). - - Returns: - None. This method updates the internal state of the optimizer. - - Note: - - This method assumes that the caller handles any necessary synchronization if used in a parallel context - (e.g., using locks when updating shared state). - - Raises: - ValueError: If the input shapes are inconsistent or if y is not a scalar when x is a single point. - - Examples: - >>> import numpy as np - >>> from spotoptim import SpotOptim - >>> opt = SpotOptim( - ... fun=lambda X: np.sum(X**2, axis=1), - ... bounds=[(-5, 5), (-5, 5)], - ... n_jobs=2 - ... ) - >>> opt._update_storage_steady(np.array([1.0, 2.0]), 5.0) - >>> print(opt.X_) - [[1. 2.]] - >>> print(opt.y_) - [5.] - >>> print(opt.best_x_) - [1. 2.] - >>> print(opt.best_y_) - 5.0 - - """ - _steady.update_storage_steady(self, x, y) - - def optimize_steady_state( - self, - timeout_start: float, - X0: Optional[np.ndarray], - y0_known: Optional[float] = None, - max_iter_override: Optional[int] = None, - ) -> Tuple[str, OptimizeResult]: - """Perform steady-state asynchronous optimization (n_jobs > 1). - This method implements a hybrid steady-state parallelization strategy. - The executor types are selected at runtime based on GIL availability: - Standard GIL build (Python ≤ 3.12 or GIL-enabled 3.13+): - * ``ProcessPoolExecutor`` (``eval_pool``) — objective function evaluations. - Process isolation ensures arbitrary callables (lambdas, closures) - serialized with ``dill`` run safely without touching shared state. - * ``ThreadPoolExecutor`` (``search_pool``) — surrogate search tasks. - Threads share the main-process heap; zero ``dill`` overhead. - A ``threading.Lock`` (``_surrogate_lock``) prevents a surrogate refit - from racing with an in-flight search thread. - Free-threaded build (``python3.13t`` / ``--disable-gil``): - * Both ``eval_pool`` and ``search_pool`` are ``ThreadPoolExecutor`` - instances. Threads achieve true CPU-level parallelism without the GIL. - The ``dill`` serialization step for eval tasks is eliminated — ``fun`` - is called directly from the shared heap. The ``_surrogate_lock`` is - still used to serialize surrogate reads and refits. - Pipeline: - 1. Parallel Initial Design: - ``n_initial`` points are dispatched to ``eval_pool``. Results are - collected via ``FIRST_COMPLETED`` until all initial evaluations finish. - 2. First Surrogate Fit: - Called on the main thread once all initial evaluations are in. - No lock is needed here because no search threads are active yet. - 3. Parallel Search (Thread Pool): - Up to ``n_jobs`` search tasks are submitted to ``search_pool``. - Each acquires ``_surrogate_lock`` before calling - ``suggest_next_infill_point()``, serializing concurrent surrogate reads. - 4. Steady-State Loop with Batch Dispatch: - - Search completes → candidate appended to ``pending_cands``. - - When ``len(pending_cands) >= eval_batch_size`` (or no search tasks - remain), all pending candidates are stacked into ``X_batch`` and - dispatched as a single eval call to ``eval_pool``. - On GIL builds this calls ``remote_batch_eval_wrapper`` (dill); - on free-threaded builds it calls ``fun`` directly in a thread. - - Batch eval completes → storage updated for every point, surrogate - refit once under ``_surrogate_lock``, new search slots filled. - - ``eval_batch_size=1`` (default) dispatches immediately on each - search completion, preserving the original one-point behavior. - - This cycle continues until ``max_iter`` evaluations or ``max_time`` - minutes is reached. - The optimization terminates when either: - - Total function evaluations reach ``max_iter`` (including initial design), OR - - Runtime exceeds ``max_time`` minutes - - Args: - timeout_start (float): Start time for timeout. - X0 (Optional[np.ndarray]): Initial design points in Natural Space, shape (n_initial, n_features). - y0_known (Optional[float]): Known best objective value from a previous run. - When provided together with ``self.x0``, the matching point in the initial - design is pre-filled with this value and not re-submitted to the worker - pool, saving one evaluation per restart (restart injection). - max_iter_override (Optional[int]): Override for maximum number of iterations. - - Raises: - RuntimeError: If all initial design evaluations fail, likely due to - pickling issues or missing imports in the worker process. - The error message provides guidance on how to address this issue. - - Returns: - Tuple[str, OptimizeResult]: Tuple containing status and optimization result. - - Examples: - ```{python} - import time - from spotoptim import SpotOptim - from spotoptim.function import sphere - opt = SpotOptim( - fun=sphere, - bounds=[(-5, 5), (-5, 5)], - n_initial=5, - max_iter=10, - seed=0, - n_jobs=2, - ) - status, result = opt.optimize_steady_state(timeout_start=time.time(), X0=None) - print(status) - print(result.message.splitlines()[0]) - ``` - """ - return _steady.optimize_steady_state( - self, timeout_start, X0, y0_known, max_iter_override - ) - # ==================== # TASK_MO: # * store_mo() @@ -5228,6 +4977,8 @@ def test_func(X): opt.plot_progress() ``` """ + from spotoptim.plot.visualization import plot_progress + plot_progress( self, show=show, log_y=log_y, figsize=figsize, ylabel=ylabel, mo=mo ) @@ -5289,6 +5040,8 @@ def test_func(X): opt.plot_surrogate() ``` """ + from spotoptim.plot.visualization import plot_surrogate + plot_surrogate( self, i=i, @@ -5355,6 +5108,8 @@ def test_func(X): opt.plot_important_hyperparameter_contour(max_imp=2) ``` """ + from spotoptim.plot.visualization import plot_important_hyperparameter_contour + plot_important_hyperparameter_contour( self, max_imp=max_imp, @@ -5382,6 +5137,8 @@ def _plot_surrogate_with_factors( figsize: Tuple[int, int] = (12, 10), ) -> None: """Delegates to spotoptim.plot.visualization._plot_surrogate_with_factors.""" + from spotoptim.plot.visualization import _plot_surrogate_with_factors + _plot_surrogate_with_factors( self, i=i, @@ -5445,6 +5202,13 @@ def test_func(X): names, values = zip(*filtered_data) + try: + import matplotlib.pyplot as plt + except ImportError as e: + raise ImportError( + "plot_importance requires matplotlib. Install with: pip install 'spotoptim[viz]'" + ) from e + plt.figure(figsize=figsize) y_pos = np.arange(len(names)) plt.barh(y_pos, values, align="center") @@ -5512,21 +5276,15 @@ def objective(X): """ try: import matplotlib.pyplot as plt - except ImportError: + except ImportError as e: raise ImportError( - "matplotlib is required for plot_parameter_scatter(). " - "Install it with: pip install matplotlib" - ) + "plot_parameter_scatter() requires matplotlib. " + "Install it with: pip install 'spotoptim[viz]'" + ) from e - # Import scipy if correlation is requested + # scipy is a core dependency if show_correlation: - try: - from scipy.stats import spearmanr - except ImportError: - raise ImportError( - "scipy is required for show_correlation=True. " - "Install it with: pip install scipy" - ) + from scipy.stats import spearmanr if self.X_ is None or self.y_ is None or len(self.y_) == 0: raise ValueError("No optimization data available. Run optimize() first.") @@ -5689,12 +5447,16 @@ def objective(X): def _generate_mesh_grid(self, i: int, j: int, num: int = 100): # Wrapper for _generate_mesh_grid from visualization module. + from spotoptim.plot.visualization import _generate_mesh_grid + return _generate_mesh_grid(self, i, j, num) def _generate_mesh_grid_with_factors( self, i: int, j: int, num: int, is_factor_i: bool, is_factor_j: bool ): # Wrapper for _generate_mesh_grid_with_factors from visualization module. + from spotoptim.plot.visualization import _generate_mesh_grid_with_factors + return _generate_mesh_grid_with_factors( self, i, j, num, is_factor_i, is_factor_j ) diff --git a/src/spotoptim/__init__.py b/src/spotoptim/__init__.py index 89ca4688..5ab3a417 100644 --- a/src/spotoptim/__init__.py +++ b/src/spotoptim/__init__.py @@ -12,19 +12,8 @@ __version__ = "unknown" from .SpotOptim import SpotOptim, SpotOptimConfig, SpotOptimState -from .surrogate import Kriging, SimpleKriging, MLPSurrogate -from .nn import MLP, LinearRegressor -from .data import DiabetesDataset, get_diabetes_dataloaders +from .surrogate import Kriging, SimpleKriging from .tricands import tricands -from .utils import ( - get_pca, - plot_pca_scree, - plot_pca1vs2, - get_pca_topk, - get_loading_scores, - plot_loading_scores, - TorchStandardScaler, -) __all__ = [ "SpotOptim", @@ -32,17 +21,5 @@ "SpotOptimState", "Kriging", "SimpleKriging", - "MLPSurrogate", - "MLP", - "LinearRegressor", - "DiabetesDataset", - "get_diabetes_dataloaders", "tricands", - "get_pca", - "plot_pca_scree", - "plot_pca1vs2", - "get_pca_topk", - "get_loading_scores", - "plot_loading_scores", - "TorchStandardScaler", ] diff --git a/src/spotoptim/core/data.py b/src/spotoptim/core/data.py index c94d40b3..d91ed794 100644 --- a/src/spotoptim/core/data.py +++ b/src/spotoptim/core/data.py @@ -2,11 +2,15 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later +from __future__ import annotations + from abc import ABC, abstractmethod -from typing import Any, Tuple, Optional, Union -import numpy as np -import torch -from torch.utils.data import Dataset +from typing import Any, Tuple, Optional, Union, TYPE_CHECKING + +if TYPE_CHECKING: + import numpy as np + import torch + from torch.utils.data import Dataset class SpotDataSet(ABC): @@ -74,25 +78,15 @@ def __init__( self.x_test = x_test self.y_test = y_test - def get_train_data( - self, - ) -> Tuple[Union[np.ndarray, torch.Tensor], Union[np.ndarray, torch.Tensor]]: + def get_train_data(self) -> Tuple: return self.x_train, self.y_train - def get_validation_data( - self, - ) -> Optional[ - Tuple[Union[np.ndarray, torch.Tensor], Union[np.ndarray, torch.Tensor]] - ]: + def get_validation_data(self) -> Optional[Tuple]: if self.x_val is not None: return self.x_val, self.y_val return None - def get_test_data( - self, - ) -> Optional[ - Tuple[Union[np.ndarray, torch.Tensor], Union[np.ndarray, torch.Tensor]] - ]: + def get_test_data(self) -> Optional[Tuple]: if self.x_test is not None: return self.x_test, self.y_test return None @@ -105,11 +99,11 @@ class SpotDataFromTorchDataset(SpotDataSet): def __init__( self, - train_dataset: Dataset, + train_dataset, input_dim: int, output_dim: int, - val_dataset: Optional[Dataset] = None, - test_dataset: Optional[Dataset] = None, + val_dataset=None, + test_dataset=None, target_column: Optional[str] = None, ): super().__init__(input_dim, output_dim, target_column) diff --git a/src/spotoptim/core/experiment.py b/src/spotoptim/core/experiment.py index d97118c5..17498c4c 100644 --- a/src/spotoptim/core/experiment.py +++ b/src/spotoptim/core/experiment.py @@ -4,7 +4,6 @@ from dataclasses import dataclass, field from typing import Any, List, Optional, Dict -import torch from spotoptim.core.data import SpotDataSet @@ -116,12 +115,15 @@ def to_dict(self) -> Dict[str, Any]: return self.__dict__.copy() @property - def torch_device(self) -> torch.device: + def torch_device(self): """Returns the torch.device object. Returns: torch.device: The torch device object. + Raises: + ImportError: If PyTorch is not installed. + Examples: ```{python} import torch @@ -139,4 +141,10 @@ def torch_device(self) -> torch.device: print(exp.torch_device) ``` """ + try: + import torch + except ImportError as e: + raise ImportError( + "ExperimentControl.torch_device requires PyTorch. Install with: pip install 'spotoptim[torch]'" + ) from e return torch.device(self.device) diff --git a/src/spotoptim/core/protocol.py b/src/spotoptim/core/protocol.py index 0fcfa382..015a4a61 100644 --- a/src/spotoptim/core/protocol.py +++ b/src/spotoptim/core/protocol.py @@ -70,8 +70,6 @@ class SpotOptimProtocol(Protocol): window_size: Optional[int] min_tol_metric: str prob_surrogate: Optional[List[float]] - n_jobs: int - eval_batch_size: int acquisition_optimizer_kwargs: Optional[Dict[str, Any]] args: Tuple kwargs: Optional[Dict[str, Any]] @@ -172,7 +170,6 @@ def _predict_with_uncertainty( def update_stats(self) -> None: ... def update_success_rate(self, y_new: np.ndarray) -> None: ... - def _update_storage_steady(self, x: np.ndarray, y: float) -> None: ... def _init_tensorboard(self) -> None: ... def _close_and_del_tensorboard_writer(self) -> None: ... diff --git a/src/spotoptim/data/__init__.py b/src/spotoptim/data/__init__.py index 9e1e34ad..d7f16092 100644 --- a/src/spotoptim/data/__init__.py +++ b/src/spotoptim/data/__init__.py @@ -8,7 +8,25 @@ machine learning tasks. """ -from .diabetes import DiabetesDataset, get_diabetes_dataloaders from .base import Config, FileConfig __all__ = ["DiabetesDataset", "get_diabetes_dataloaders", "Config", "FileConfig"] + +_lazy_map = { + "DiabetesDataset": ("spotoptim.data.diabetes", "DiabetesDataset"), + "get_diabetes_dataloaders": ("spotoptim.data.diabetes", "get_diabetes_dataloaders"), +} + + +def __getattr__(name: str): + if name in _lazy_map: + module_path, attr = _lazy_map[name] + import importlib + + module = importlib.import_module(module_path) + return getattr(module, attr) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def __dir__(): + return sorted(__all__) diff --git a/src/spotoptim/function/__init__.py b/src/spotoptim/function/__init__.py index d645b626..34737ce4 100644 --- a/src/spotoptim/function/__init__.py +++ b/src/spotoptim/function/__init__.py @@ -35,8 +35,6 @@ zdt6, ) from .forr08a import aerofoilcd, branin, onevar -from .torch_objective import TorchObjective -from .remote import objective_remote __all__ = [ "sphere", @@ -69,3 +67,22 @@ "TorchObjective", "objective_remote", ] + +_lazy_map = { + "TorchObjective": ("spotoptim.function.torch_objective", "TorchObjective"), + "objective_remote": ("spotoptim.function.remote", "objective_remote"), +} + + +def __getattr__(name: str): + if name in _lazy_map: + module_path, attr = _lazy_map[name] + import importlib + + module = importlib.import_module(module_path) + return getattr(module, attr) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def __dir__(): + return sorted(__all__) diff --git a/src/spotoptim/nn/__init__.py b/src/spotoptim/nn/__init__.py index e2a6ac67..6842e11f 100644 --- a/src/spotoptim/nn/__init__.py +++ b/src/spotoptim/nn/__init__.py @@ -4,7 +4,23 @@ """Neural network models for spotoptim.""" -from .linear_regressor import LinearRegressor -from .mlp import MLP - __all__ = ["LinearRegressor", "MLP"] + +_lazy_map = { + "LinearRegressor": ("spotoptim.nn.linear_regressor", "LinearRegressor"), + "MLP": ("spotoptim.nn.mlp", "MLP"), +} + + +def __getattr__(name: str): + if name in _lazy_map: + module_path, attr = _lazy_map[name] + import importlib + + module = importlib.import_module(module_path) + return getattr(module, attr) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def __dir__(): + return sorted(__all__) diff --git a/src/spotoptim/optimizer/__init__.py b/src/spotoptim/optimizer/__init__.py index 822b1530..8ac150df 100644 --- a/src/spotoptim/optimizer/__init__.py +++ b/src/spotoptim/optimizer/__init__.py @@ -2,6 +2,22 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later -from .schedule_free import AdamWScheduleFree - __all__ = ["AdamWScheduleFree"] + +_lazy_map = { + "AdamWScheduleFree": ("spotoptim.optimizer.schedule_free", "AdamWScheduleFree"), +} + + +def __getattr__(name: str): + if name in _lazy_map: + module_path, attr = _lazy_map[name] + import importlib + + module = importlib.import_module(module_path) + return getattr(module, attr) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def __dir__(): + return sorted(__all__) diff --git a/src/spotoptim/optimizer/steady_state.py b/src/spotoptim/optimizer/steady_state.py deleted file mode 100644 index d7c60a7d..00000000 --- a/src/spotoptim/optimizer/steady_state.py +++ /dev/null @@ -1,380 +0,0 @@ -# SPDX-FileCopyrightText: 2026 bartzbeielstein -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -"""Steady-state parallel optimization loop.""" - -from __future__ import annotations - -import threading -import time -from typing import TYPE_CHECKING, Optional, Tuple - -import dill -import numpy as np -from scipy.optimize import OptimizeResult - -from spotoptim.utils.parallel import ( - is_gil_disabled, - remote_batch_eval_wrapper, - remote_eval_wrapper, -) - -if TYPE_CHECKING: - from spotoptim.core.protocol import SpotOptimProtocol - - -def update_storage_steady(optimizer: SpotOptimProtocol, x, y): - """Helper to safely append single point (for steady state). - - The evaluated point arrives in internal (transformed, reduced) scale -- the - representation produced by ``get_initial_design`` and - ``suggest_next_infill_point``. It is converted to natural scale via - ``inverse_transform_X`` before storage, mirroring the sequential - ``update_storage`` path (:mod:`spotoptim.core.storage`) so that ``X_`` and - ``best_x_`` hold user-facing original-scale values regardless of ``n_jobs``. - Without this conversion a transformed variable (e.g. ``log10``) is stored in - transformed space and then re-transformed when the surrogate is refit, - producing ``NaN`` and crashing the Gaussian-process fit. - - Args: - optimizer: SpotOptim instance. - x (ndarray): New point(s) in internal scale, shape (n_features,) or - (N, n_features). - y (float or ndarray): Corresponding function value(s). - """ - x = np.atleast_2d(x) - x = optimizer.inverse_transform_X(x) - # Natural-scale repair mirrors evaluate_function (issue #87): transformed - # int dims are rounded and clipped to the declared bounds before storage. - x = optimizer.repair_natural_X(x) - if optimizer.X_ is None: - optimizer.X_ = x - optimizer.y_ = np.array([y]) - else: - optimizer.X_ = np.vstack([optimizer.X_, x]) - optimizer.y_ = np.append(optimizer.y_, y) - - # Update best - if optimizer.best_y_ is None or y < optimizer.best_y_: - optimizer.best_y_ = y - optimizer.best_x_ = x.flatten() - - optimizer.min_y = optimizer.best_y_ - optimizer.min_X = optimizer.best_x_ - - -def optimize_steady_state( - optimizer: SpotOptimProtocol, - timeout_start: float, - X0: Optional[np.ndarray], - y0_known: Optional[float] = None, - max_iter_override: Optional[int] = None, -) -> Tuple[str, OptimizeResult]: - """Perform steady-state asynchronous optimization (n_jobs > 1). - - Args: - optimizer: SpotOptim instance. - timeout_start (float): Start time for timeout. - X0 (Optional[np.ndarray]): Initial design points in Natural Space. - y0_known (Optional[float]): Known best objective value from a previous run. - max_iter_override (Optional[int]): Override for maximum number of iterations. - - Raises: - RuntimeError: If all initial design evaluations fail. - - Returns: - Tuple[str, OptimizeResult]: Tuple containing status and optimization result. - """ - # Setup similar to _optimize_single_run - optimizer.set_seed() - X0 = optimizer.get_initial_design(X0) - X0 = optimizer.curate_initial_design(X0) - - # Restart injection - y0_prefilled = np.full(len(X0), np.nan) - if y0_known is not None and optimizer.x0 is not None: - dists = np.linalg.norm(X0 - optimizer.x0, axis=1) - matches = dists < 1e-9 - if np.any(matches): - if optimizer.verbose: - print("Skipping re-evaluation of injected best point.") - y0_prefilled[matches] = y0_known - - effective_max_iter = ( - max_iter_override if max_iter_override is not None else optimizer.max_iter - ) - - from contextlib import ExitStack - from concurrent.futures import ( - ProcessPoolExecutor, - ThreadPoolExecutor, - wait, - FIRST_COMPLETED, - ) - - _no_gil = is_gil_disabled() - _surrogate_lock = threading.Lock() - - def _thread_search_task(): - """Search task for ThreadPoolExecutor: direct call, no dill.""" - with _surrogate_lock: - return optimizer.suggest_next_infill_point() - - def _thread_eval_task_single(x): - """Thread-based single-point eval for free-threaded Python (no dill).""" - try: - x_2d = x.reshape(1, -1) - y_arr = optimizer.evaluate_function(x_2d) - return x, y_arr[0] - except Exception as e: - return None, e - - def _thread_batch_eval_task(X_batch): - """Thread-based batch eval for free-threaded Python (no dill).""" - try: - y_batch = optimizer.evaluate_function(X_batch) - return X_batch, y_batch - except Exception as e: - return None, e - - with ExitStack() as _stack: - eval_pool = _stack.enter_context( - ThreadPoolExecutor(max_workers=optimizer.n_jobs) - if _no_gil - else ProcessPoolExecutor(max_workers=optimizer.n_jobs) - ) - search_pool = _stack.enter_context( - ThreadPoolExecutor(max_workers=optimizer.n_jobs) - ) - futures = {} - - # --- Phase 1: Initial Design Evaluation --- - n_to_submit = 0 - for i, x in enumerate(X0): - if np.isfinite(y0_prefilled[i]): - optimizer._update_storage_steady(x, y0_prefilled[i]) - continue - if _no_gil: - fut = eval_pool.submit(_thread_eval_task_single, x) - else: - _tb_writer_temp = optimizer.tb_writer - optimizer.tb_writer = None - try: - pickled_args = dill.dumps((optimizer, x)) - finally: - optimizer.tb_writer = _tb_writer_temp - fut = eval_pool.submit(remote_eval_wrapper, pickled_args) - futures[fut] = "eval" - n_to_submit += 1 - - if optimizer.verbose: - n_injected = int(np.sum(np.isfinite(y0_prefilled))) - suffix = ( - f" ({n_injected} injected from restart, skipped re-evaluation)." - if n_injected - else "." - ) - print( - f"Submitted {n_to_submit} initial points for parallel evaluation{suffix}" - ) - - # Wait for all submitted initial evaluations to complete. - initial_done_count = 0 - while initial_done_count < n_to_submit: - done, _ = wait(futures.keys(), return_when=FIRST_COMPLETED) - for fut in done: - ftype = futures.pop(fut) - if ftype != "eval": - continue - - try: - x_done, y_done = fut.result() - if isinstance(y_done, Exception): - if optimizer.verbose: - print(f"Eval failed: {y_done}") - else: - optimizer._update_storage_steady(x_done, y_done) - except Exception as e: - if optimizer.verbose: - print(f"Task failed: {e}") - - initial_done_count += 1 - - # Init tensorboard and stats - optimizer._init_tensorboard() - - if optimizer.y_ is None or len(optimizer.y_) == 0: - raise RuntimeError( - "All initial design evaluations failed. " - "Check your objective function for pickling issues or missing imports " - "(e.g. numpy) in the worker process. " - "If defining functions in a notebook/script, ensure imports are inside " - "the function." - ) - - optimizer.update_stats() - optimizer.get_best_xy_initial_design() - - # Fit first surrogate (no lock needed — no search threads active yet) - if optimizer.verbose: - print( - f"Initial design evaluated. Fitting surrogate... " - f"(Data size: {len(optimizer.y_)})" - ) - optimizer.fit_scheduler() - - # --- Phase 2: Steady State Loop --- - if optimizer.verbose: - print("Starting steady-state optimization loop...") - - pending_cands: list = [] - _future_n_pts: dict = {} - - def _flush_batch() -> None: - """Dispatch all pending_cands as a single batch eval task.""" - nonlocal pending_cands - X_batch = np.vstack(pending_cands) - n_in_batch = len(pending_cands) - pending_cands = [] - if _no_gil: - fut_eval = eval_pool.submit(_thread_batch_eval_task, X_batch) - else: - _tb_writer_temp = optimizer.tb_writer - optimizer.tb_writer = None - try: - pickled_args = dill.dumps((optimizer, X_batch)) - finally: - optimizer.tb_writer = _tb_writer_temp - fut_eval = eval_pool.submit(remote_batch_eval_wrapper, pickled_args) - futures[fut_eval] = "batch_eval" - _future_n_pts[fut_eval] = n_in_batch - - def _batch_ready() -> bool: - """True when pending_cands should be flushed to eval_pool.""" - if not pending_cands: - return False - if len(pending_cands) >= optimizer.eval_batch_size: - return True - return not any(t == "search" for t in futures.values()) - - while (len(optimizer.y_) < effective_max_iter) and ( - time.time() < timeout_start + optimizer.max_time * 60 - ): - if _batch_ready(): - _flush_batch() - - n_active = len(futures) - n_slots = optimizer.n_jobs - n_active - - if n_slots > 0: - for _ in range(n_slots): - n_in_flight = sum(_future_n_pts.values()) - n_searches = sum(1 for t in futures.values() if t == "search") - reserved = ( - len(optimizer.y_) - + n_in_flight - + n_searches - + len(pending_cands) - ) - if reserved < effective_max_iter: - fut = search_pool.submit(_thread_search_task) - futures[fut] = "search" - else: - break - - if _batch_ready(): - _flush_batch() - - if not futures and not pending_cands: - break - - if not futures: - _flush_batch() - continue - - done, _ = wait(futures.keys(), return_when=FIRST_COMPLETED) - for fut in done: - ftype = futures.pop(fut) - try: - res = fut.result() - if isinstance(res, Exception): - if optimizer.verbose: - print(f"Remote {ftype} failed: {res}") - _future_n_pts.pop(fut, None) - continue - - if ftype == "search": - x_cand = res - pending_cands.append(x_cand) - if _batch_ready(): - _flush_batch() - - elif ftype == "batch_eval": - _future_n_pts.pop(fut, None) - X_done, y_done = res - if isinstance(y_done, Exception): - if optimizer.verbose: - print(f"Batch eval failed: {y_done}") - else: - for xi, yi in zip(X_done, y_done): - optimizer.update_success_rate(np.array([yi])) - optimizer._update_storage_steady(xi, yi) - optimizer.n_iter_ += 1 - - # TensorBoard: this result loop runs only in the - # parent main thread (workers carry tb_writer=None, - # search threads never touch the writer), so the - # single SummaryWriter needs no lock. update_stats() - # refreshes ``counter`` (the TB step) which the - # steady-state loop otherwise never advances; it is - # kept inside the guard so the no-TB path stays - # byte-identical. Mirrors the sequential per-eval - # logging in the main optimize loop. - if optimizer.tb_writer is not None: - optimizer.update_stats() - for xi, yi in zip(X_done, y_done): - optimizer._write_tensorboard_hparams( - np.asarray(xi, dtype=float), float(yi) - ) - optimizer._write_tensorboard_scalars() - - if optimizer.verbose: - if optimizer.max_time != np.inf: - prog_val = ( - (time.time() - timeout_start) - / (optimizer.max_time * 60) - * 100 - ) - progress_str = f"Time: {prog_val:.1f}%" - else: - prog_val = ( - len(optimizer.y_) / effective_max_iter * 100 - ) - progress_str = f"Evals: {prog_val:.1f}%" - - print( - f"Iter {len(optimizer.y_)}/{effective_max_iter}" - f" | Best: {optimizer.best_y_:.6f}" - f" | Rate: {optimizer.success_rate:.2f}" - f" | {progress_str}" - ) - - with _surrogate_lock: - optimizer.fit_scheduler() - - except Exception as e: - _future_n_pts.pop(fut, None) - if optimizer.verbose: - print(f"Error processing future: {e}") - - return "FINISHED", OptimizeResult( - x=optimizer.best_x_, - fun=optimizer.best_y_, - nfev=len(optimizer.y_), - nit=optimizer.n_iter_, - success=True, - message="Optimization finished (Steady State)", - X=optimizer.X_, - y=optimizer.y_, - ) diff --git a/src/spotoptim/plot/__init__.py b/src/spotoptim/plot/__init__.py index 4ba46619..d8da9655 100644 --- a/src/spotoptim/plot/__init__.py +++ b/src/spotoptim/plot/__init__.py @@ -2,5 +2,10 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later -from .contour import * # noqa: F401, F403 -from .mo import * # noqa: F401, F403 +"""Plotting and visualization utilities for SpotOptim. + +Plotting helpers live in submodules (``contour``, ``mo``, ``visualization``) +and import matplotlib lazily, so importing this package does not pull in +matplotlib. Import the helper you need directly from its submodule, e.g. +``from spotoptim.plot.visualization import plot_progress``. +""" diff --git a/src/spotoptim/sampling/effects.py b/src/spotoptim/sampling/effects.py index 9e5ee1e5..6acb2437 100644 --- a/src/spotoptim/sampling/effects.py +++ b/src/spotoptim/sampling/effects.py @@ -3,7 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-or-later import numpy as np -import matplotlib.pyplot as plt import pandas as pd from sklearn.ensemble import GradientBoostingRegressor from sklearn.inspection import PartialDependenceDisplay @@ -295,6 +294,12 @@ def screening_plot(X, fun, xi, p, labels, bounds=None, show=True) -> None: print=False, ) """ + try: + import matplotlib.pyplot as plt + except ImportError as e: + raise ImportError( + "screening_plot requires matplotlib. Install with: pip install 'spotoptim[viz]'" + ) from e k = X.shape[1] sm, ssd = _screening(X=X, fun=fun, xi=xi, p=p, labels=labels, bounds=bounds) plt.figure() @@ -346,6 +351,12 @@ def plot_all_partial_dependence( """ + try: + import matplotlib.pyplot as plt + except ImportError as e: + raise ImportError( + "plot_all_partial_dependence requires matplotlib. Install with: pip install 'spotoptim[viz]'" + ) from e # Separate features and target X = df y = df_target # Target variable is now a Series diff --git a/src/spotoptim/sampling/mm.py b/src/spotoptim/sampling/mm.py index 99952e43..5373a7b2 100644 --- a/src/spotoptim/sampling/mm.py +++ b/src/spotoptim/sampling/mm.py @@ -5,7 +5,6 @@ import numpy as np import pandas as pd from typing import Tuple, Optional -import matplotlib.pyplot as plt from spotoptim.utils.stats import normalize_X from spotoptim.sampling.lhs import rlh from scipy.stats.qmc import LatinHypercube @@ -498,6 +497,12 @@ def mmlhs( # Simple visualization of the first two dimensions if plot and (X_best.shape[1] >= 2): + try: + import matplotlib.pyplot as plt + except ImportError as e: + raise ImportError( + "mmlhs visualization requires matplotlib. Install with: pip install 'spotoptim[viz]'" + ) from e plt.clf() plt.scatter(X_best[:, 0], X_best[:, 1], marker="o") plt.grid(True) @@ -1414,6 +1419,12 @@ def bestlh( # Plot the first two dimensions if plot and (k >= 2): + try: + import matplotlib.pyplot as plt + except ImportError as e: + raise ImportError( + "bestlh visualization requires matplotlib. Install with: pip install 'spotoptim[viz]'" + ) from e plt.scatter(X[:, 0], X[:, 1], c="r", marker="o") plt.title(f"Morris-Mitchell optimum plan found using q={q_list[best_idx]}") plt.xlabel("x_1") @@ -1453,6 +1464,12 @@ def plot_mmphi_vs_n_lhs( >>> from spotoptim.sampling.mm import plot_mmphi_vs_n_lhs >>> plot_mmphi_vs_n_lhs(k_dim=3, seed=42, n_min=10, n_max=50, n_step=5, q_phi=2.0, p_phi=2.0) """ + try: + import matplotlib.pyplot as plt + except ImportError as e: + raise ImportError( + "plot_mmphi_vs_n_lhs requires matplotlib. Install with: pip install 'spotoptim[viz]'" + ) from e n_values = list(range(n_min, n_max + 1, n_step)) if not n_values: print("Warning: n_values list is empty. Check n_min, n_max, and n_step.") @@ -1557,6 +1574,12 @@ def plot_mmphi_vs_points( >>> # Plot mmphi vs number of added points >>> df_summary = plot_mmphi_vs_points(X_base, x_min, x_max, p_min=10, p_max=50, p_step=10, n_repeats=3) """ + try: + import matplotlib.pyplot as plt + except ImportError as e: + raise ImportError( + "plot_mmphi_vs_points requires matplotlib. Install with: pip install 'spotoptim[viz]'" + ) from e n, m = X_base.shape p_values = range(p_min, p_max + 1, p_step) @@ -1689,6 +1712,12 @@ def plot_mmphi_corrected_vs_points( >>> x_max = np.array([1.0, 1.0]) >>> df_summary = plot_mmphi_corrected_vs_points(X_base, x_min, x_max, p_min=10, p_max=50, p_step=10, n_repeats=3) """ + try: + import matplotlib.pyplot as plt + except ImportError as e: + raise ImportError( + "plot_mmphi_corrected_vs_points requires matplotlib. Install with: pip install 'spotoptim[viz]'" + ) from e _, m = X_base.shape p_values = range(p_min, p_max + 1, p_step) @@ -1770,6 +1799,12 @@ def mm_improvement_contour( mm_improvement_contour(X_base) """ + try: + import matplotlib.pyplot as plt + except ImportError as e: + raise ImportError( + "mm_improvement_contour requires matplotlib. Install with: pip install 'spotoptim[viz]'" + ) from e _, J_base, d_base = mmphi_intensive(X_base, q=2, p=2) X1, X2 = np.meshgrid(x1, x2) improvement_grid = np.zeros(X1.shape) @@ -1842,6 +1877,12 @@ def plot_mmphi_corrected_vs_n_lhs( >>> plot_mmphi_corrected_vs_n_lhs(k_dim=3, seed=42, n_min=10, n_max=50, n_step=5, q_phi=2.0, p_phi=2.0) >>> plot_mmphi_corrected_vs_n_lhs(k_dim=3, seed=42, n_min=10, n_max=50, plot_only_corrected=True) """ + try: + import matplotlib.pyplot as plt + except ImportError as e: + raise ImportError( + "plot_mmphi_corrected_vs_n_lhs requires matplotlib. Install with: pip install 'spotoptim[viz]'" + ) from e n_values = list(range(n_min, n_max + 1, n_step)) if not n_values: print("Warning: n_values list is empty. Check n_min, n_max, and n_step.") diff --git a/src/spotoptim/surrogate/__init__.py b/src/spotoptim/surrogate/__init__.py index 297ab333..7c42c897 100644 --- a/src/spotoptim/surrogate/__init__.py +++ b/src/spotoptim/surrogate/__init__.py @@ -35,6 +35,23 @@ from .kriging import Kriging from .simple_kriging import SimpleKriging -from .mlp_surrogate import MLPSurrogate __all__ = ["Kriging", "SimpleKriging", "MLPSurrogate"] + +_lazy_map = { + "MLPSurrogate": ("spotoptim.surrogate.mlp_surrogate", "MLPSurrogate"), +} + + +def __getattr__(name: str): + if name in _lazy_map: + module_path, attr = _lazy_map[name] + import importlib + + module = importlib.import_module(module_path) + return getattr(module, attr) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def __dir__(): + return sorted(__all__) diff --git a/src/spotoptim/surrogate/mlp_surrogate.py b/src/spotoptim/surrogate/mlp_surrogate.py index 7e5afd2f..6d328d40 100644 --- a/src/spotoptim/surrogate/mlp_surrogate.py +++ b/src/spotoptim/surrogate/mlp_surrogate.py @@ -13,14 +13,9 @@ from typing import List, Optional, Tuple, Union import numpy as np -import torch -import torch.nn as nn -from torch.utils.data import DataLoader, TensorDataset from sklearn.base import BaseEstimator, RegressorMixin from sklearn.preprocessing import StandardScaler -from spotoptim.nn.mlp import MLP - class MLPSurrogate(BaseEstimator, RegressorMixin): """ @@ -116,6 +111,17 @@ def fit(self, X: np.ndarray, y: np.ndarray) -> "MLPSurrogate": Returns: MLPSurrogate: The fitted model. """ + try: + import torch + import torch.nn as nn + from torch.utils.data import DataLoader, TensorDataset + except ImportError as e: + raise ImportError( + "MLPSurrogate requires PyTorch. Install with: pip install 'spotoptim[torch]'" + ) from e + + from spotoptim.nn.mlp import MLP + # Set seeds for reproducibility torch.manual_seed(self.seed) np.random.seed(self.seed) @@ -229,6 +235,13 @@ def predict( np.ndarray: Predicted mean values, shape (n_samples,). tuple: (mean, std) if return_std is True. """ + try: + import torch + except ImportError as e: + raise ImportError( + "MLPSurrogate requires PyTorch. Install with: pip install 'spotoptim[torch]'" + ) from e + if self.model_ is None: raise RuntimeError("Model is not fitted yet. Call 'fit' first.") diff --git a/src/spotoptim/tricands/tricands.py b/src/spotoptim/tricands/tricands.py index c379daf8..86a93386 100644 --- a/src/spotoptim/tricands/tricands.py +++ b/src/spotoptim/tricands/tricands.py @@ -2,7 +2,6 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later -import matplotlib.pyplot as plt from scipy.spatial import Delaunay, ConvexHull import numpy as np @@ -167,6 +166,14 @@ def tricands( if np.min(X) < lower or np.max(X) > upper: raise Exception("X outside of lower/upper bounds") + if vis: + try: + import matplotlib.pyplot as plt + except ImportError as e: + raise ImportError( + "tricands visualization requires matplotlib. Install with: pip install 'spotoptim[viz]'" + ) from e + # possible visual if vis: plt.figure() diff --git a/src/spotoptim/utils/__init__.py b/src/spotoptim/utils/__init__.py index a4cad975..5a42c884 100644 --- a/src/spotoptim/utils/__init__.py +++ b/src/spotoptim/utils/__init__.py @@ -6,24 +6,8 @@ from .boundaries import get_boundaries, map_to_original_scale from .mapping import map_lr -from .stats import normalize_X, calculate_outliers, get_combinations from .eval import mo_eval_models, mo_cv_models from .file import get_experiment_filename, get_internal_datasets_folder -from .pca import ( - get_pca, - plot_pca_scree, - plot_pca1vs2, - get_pca_topk, - get_loading_scores, - plot_loading_scores, -) -from .scaler import TorchStandardScaler -from .parallel import ( - is_gil_disabled, - remote_eval_wrapper, - remote_batch_eval_wrapper, - remote_search_task, -) __all__ = [ "get_boundaries", @@ -43,8 +27,35 @@ "get_loading_scores", "plot_loading_scores", "TorchStandardScaler", - "is_gil_disabled", - "remote_eval_wrapper", - "remote_batch_eval_wrapper", - "remote_search_task", ] + +_lazy_map = { + # stats module (matplotlib/seaborn/statsmodels imported lazily inside the + # plotting/regression helpers; these three are pure numpy/pandas/scipy) + "normalize_X": ("spotoptim.utils.stats", "normalize_X"), + "calculate_outliers": ("spotoptim.utils.stats", "calculate_outliers"), + "get_combinations": ("spotoptim.utils.stats", "get_combinations"), + # pca module (matplotlib/seaborn imported lazily inside the plot helpers) + "get_pca": ("spotoptim.utils.pca", "get_pca"), + "plot_pca_scree": ("spotoptim.utils.pca", "plot_pca_scree"), + "plot_pca1vs2": ("spotoptim.utils.pca", "plot_pca1vs2"), + "get_pca_topk": ("spotoptim.utils.pca", "get_pca_topk"), + "get_loading_scores": ("spotoptim.utils.pca", "get_loading_scores"), + "plot_loading_scores": ("spotoptim.utils.pca", "plot_loading_scores"), + # scaler (pulls torch) + "TorchStandardScaler": ("spotoptim.utils.scaler", "TorchStandardScaler"), +} + + +def __getattr__(name: str): + if name in _lazy_map: + module_path, attr = _lazy_map[name] + import importlib + + module = importlib.import_module(module_path) + return getattr(module, attr) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def __dir__(): + return sorted(__all__) diff --git a/src/spotoptim/utils/parallel.py b/src/spotoptim/utils/parallel.py deleted file mode 100644 index 6ea088be..00000000 --- a/src/spotoptim/utils/parallel.py +++ /dev/null @@ -1,178 +0,0 @@ -# SPDX-FileCopyrightText: 2026 bartzbeielstein -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -import sys - - -def is_gil_disabled() -> bool: - """Return True when running on a free-threaded (no-GIL) Python build. - Uses ``sys.is_gil_enabled()`` (public name) when available, falling back - to ``sys._is_gil_enabled()`` (CPython 3.13 internal name). On older - interpreters the attribute is absent, so the lambda default returns - ``True`` (GIL enabled), which is the safe fallback. - - Returns: - bool: ``True`` if the GIL is disabled, ``False`` otherwise. - - Examples: - ```{python} - from spotoptim.utils.parallel import is_gil_disabled - result = is_gil_disabled() - print(isinstance(result, bool)) # True - ``` - """ - checker = getattr(sys, "is_gil_enabled", None) or getattr( - sys, "_is_gil_enabled", None - ) - return not checker() if checker is not None else False - - -def remote_eval_wrapper(pickled_args): - """ - Helper function for parallel evaluation with dill. - Accepts a single argument (pickled tuple) to bypass standard pickling limitations. - - Args: - pickled_args (bytes): A pickled tuple containing (optimizer, x) where: - * optimizer: The SpotOptim instance (or surrogate model) to use for evaluation. - * x: The point at which to evaluate the objective function. - - Returns: - tuple: A tuple containing: - * x (ndarray): The input point at which the function was evaluated. - * y (ndarray or Exception): The function value(s) at x, or an Exception if evaluation failed. - - Raises: - Exception: Any exception raised during the evaluation of the objective function is caught and returned as part of the output tuple. - This allows the optimization process to continue even if some evaluations fail, and provides information about the failure for debugging. - - Examples: - ```{python} - import numpy as np - from spotoptim.utils.parallel import remote_eval_wrapper - class DummyOptimizer: - def evaluate_function(self, X): - return np.sum(X**2, axis=1) - optimizer = DummyOptimizer() - x = np.array([1.0, 2.0]) - import dill - pickled_args = dill.dumps((optimizer, x)) - x_eval, y_eval = remote_eval_wrapper(pickled_args) - print(np.allclose(x_eval, x)) - print(np.isclose(y_eval, 5.0)) - ``` - """ - try: - # Import dill locally to ensure it's available in workers - import dill - - optimizer, x = dill.loads(pickled_args) - - # Recast to 2D for evaluate_function - x_2d = x.reshape(1, -1) - y_arr = optimizer.evaluate_function(x_2d) - return x, y_arr[0] - except Exception as e: - return None, e - - -def remote_batch_eval_wrapper(pickled_args): - """Helper for parallel batch evaluation with dill. - Evaluates a batch of candidate points in a single call to ``fun(X_batch)``, - spreading process-spawn and IPC overhead across the whole batch. - - Args: - pickled_args (bytes): A pickled tuple ``(optimizer, X_batch)`` where - ``X_batch`` has shape ``(n, d)`` — ``n`` candidate points, ``d`` - dimensions each. - - Returns: - tuple: ``(X_batch, y_batch)`` on success where ``y_batch`` has shape - ``(n,)``, or ``(None, Exception)`` on failure. - - Examples: - ```{python} - import numpy as np - from spotoptim.utils.parallel remote_batch_eval_wrapper - from spotoptim import SpotOptim - import dill - - opt = SpotOptim( - fun=lambda X: np.sum(X**2, axis=1), - bounds=[(-5, 5), (-5, 5)], - n_initial=5, - max_iter=10, - ) - X_batch = np.array([[1.0, 2.0], [0.5, -0.5]]) - pickled_args = dill.dumps((opt, X_batch)) - X_out, y_out = remote_batch_eval_wrapper(pickled_args) - print(X_out.shape) # (2, 2) - print(y_out.shape) # (2,) - print(np.allclose(y_out, [5.0, 0.5])) - ``` - """ - try: - import dill - - optimizer, X_batch = dill.loads(pickled_args) - y_batch = optimizer.evaluate_function(X_batch) - return X_batch, y_batch - except Exception as e: - return None, e - - -def remote_search_task(pickled_optimizer): - """ - Helper function for parallel search with dill. - - Args: - pickled_optimizer (bytes): A pickled SpotOptim instance that has been initialized with data - and a fitted surrogate model (via X_, y_, and fit_surrogate). - - Returns: - ndarray or Exception: The suggested next infill point(s) as an array of shape (n_infill_points, n_features), - or an Exception if the operation failed. When n_infill_points=1 (default), shape is (1, n_features). - - Raises: - Exception: Any exception raised during the operation is caught and returned rather than raised, - allowing parallel execution to continue. The calling code can check if the return value is an - Exception instance and handle it appropriately. - - Examples: - ```{python} - import numpy as np - from spotoptim.utils.parallel remote_search_task - from spotoptim import SpotOptim - import dill - - # Create and initialize an optimizer - opt = SpotOptim( - fun=lambda X: np.sum(X**2, axis=1), - bounds=[(-5, 5), (-5, 5)], - n_initial=5, - max_iter=10, - ) - # Initialize with some data - np.random.seed(0) - opt.X_ = np.random.rand(10, 2) * 10 - 5 - opt.y_ = np.sum(opt.X_**2, axis=1) - opt.fit_surrogate(opt.X_, opt.y_) - - # Use the function - pickled_optimizer = dill.dumps(opt) - x_next = remote_search_task(pickled_optimizer) - isinstance(x_next, np.ndarray) - x_next.shape - # Verify the point is within bounds - (-5 <= x_next[0, 0] <= 5) and (-5 <= x_next[0, 1] <= 5) - ``` - """ - try: - import dill - - optimizer = dill.loads(pickled_optimizer) - x_new = optimizer.suggest_next_infill_point() - return x_new - except Exception as e: - return e diff --git a/src/spotoptim/utils/scaler.py b/src/spotoptim/utils/scaler.py index 80f3edd6..aedb228b 100644 --- a/src/spotoptim/utils/scaler.py +++ b/src/spotoptim/utils/scaler.py @@ -2,8 +2,6 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later -import torch - class TorchStandardScaler: """ @@ -39,7 +37,7 @@ def __init__(self): self.mean = None self.std = None - def fit(self, x: torch.Tensor) -> None: + def fit(self, x) -> None: """ Compute the mean and standard deviation of the input tensor. @@ -47,14 +45,21 @@ def fit(self, x: torch.Tensor) -> None: x (torch.Tensor): The input tensor, expected shape [n_samples, n_features] Raises: + ImportError: If PyTorch is not installed. TypeError: If the input is not a torch tensor. """ + try: + import torch + except ImportError as e: + raise ImportError( + "TorchStandardScaler requires PyTorch. Install with: pip install 'spotoptim[torch]'" + ) from e if not torch.is_tensor(x): raise TypeError("Input should be a torch tensor") self.mean = x.mean(dim=0, keepdim=True) self.std = x.std(dim=0, unbiased=False, keepdim=True) - def transform(self, x: torch.Tensor) -> torch.Tensor: + def transform(self, x): """ Scale the input tensor using the computed mean and standard deviation. @@ -65,9 +70,16 @@ def transform(self, x: torch.Tensor) -> torch.Tensor: torch.Tensor: The scaled tensor. Raises: + ImportError: If PyTorch is not installed. TypeError: If the input is not a torch tensor. RuntimeError: If the scaler has not been fitted before transforming data. """ + try: + import torch + except ImportError as e: + raise ImportError( + "TorchStandardScaler requires PyTorch. Install with: pip install 'spotoptim[torch]'" + ) from e if not torch.is_tensor(x): raise TypeError("Input should be a torch tensor") if self.mean is None or self.std is None: @@ -75,7 +87,7 @@ def transform(self, x: torch.Tensor) -> torch.Tensor: x = (x - self.mean) / (self.std + 1e-7) return x - def fit_transform(self, x: torch.Tensor) -> torch.Tensor: + def fit_transform(self, x): """ Fit the scaler to the input tensor and then scale the tensor. @@ -86,6 +98,7 @@ def fit_transform(self, x: torch.Tensor) -> torch.Tensor: torch.Tensor: The scaled tensor. Raises: + ImportError: If PyTorch is not installed. TypeError: If the input is not a torch tensor. """ self.fit(x) diff --git a/src/spotoptim/utils/stats.py b/src/spotoptim/utils/stats.py index 2260de8b..d9976e82 100644 --- a/src/spotoptim/utils/stats.py +++ b/src/spotoptim/utils/stats.py @@ -9,11 +9,6 @@ from numpy.linalg import pinv, inv, LinAlgError import copy import itertools -import matplotlib.pyplot as plt -import seaborn as sns -from statsmodels.formula.api import ols -from statsmodels.stats.outliers_influence import variance_inflation_factor -import statsmodels.api as sm from sklearn.preprocessing import OneHotEncoder @@ -464,6 +459,12 @@ def fit_all_lm(basic, xlist, data, remove_na=True) -> dict: 0 basic 1.000000 1.000000 1.000000 0.0 0.000000 3 1 x2 1.000000 1.000000 1.000000 0.0 0.000000 3} """ + try: + from statsmodels.formula.api import ols + except ImportError as e: + raise ImportError( + "fit_all_lm requires statsmodels. Install with: pip install 'spotoptim[stats]'" + ) from e # Prepare the data frame data = copy.deepcopy(data) data_cols = get_all_vars_from_formula(basic) + xlist @@ -593,6 +594,13 @@ def plot_coeff_vs_pvals( >>> estimates = fit_all_lm("y ~ x1", ["x2"], data) >>> plot_coeff_vs_pvals(estimates) """ + try: + import matplotlib.pyplot as plt + import seaborn as sns + except ImportError as e: + raise ImportError( + "plot_coeff_vs_pvals requires matplotlib and seaborn. Install with: pip install 'spotoptim[viz]'" + ) from e data = copy.deepcopy(data) if xlabels is None: xlabels = [0, 0.001, 0.01, 0.05, 0.2, 0.5, 1] @@ -693,6 +701,13 @@ def plot_coeff_vs_pvals_by_included( } plot_coeff_vs_pvals_by_included(data) """ + try: + import matplotlib.pyplot as plt + import seaborn as sns + except ImportError as e: + raise ImportError( + "plot_coeff_vs_pvals_by_included requires matplotlib and seaborn. Install with: pip install 'spotoptim[viz]'" + ) from e if xlabels is None: xlabels = [0, 0.001, 0.01, 0.05, 0.2, 0.5, 1] xbreaks = np.power(xlabels, np.log(0.5) / np.log(0.05)) @@ -810,6 +825,12 @@ def vif(X, sorted=True) -> pd.DataFrame: 1 x2 0.000000 2 x3 630.000000 """ + try: + from statsmodels.stats.outliers_influence import variance_inflation_factor + except ImportError as e: + raise ImportError( + "vif requires statsmodels. Install with: pip install 'spotoptim[stats]'" + ) from e vif_data = pd.DataFrame() vif_data["feature"] = X.columns vif_data["VIF"] = [ @@ -977,6 +998,12 @@ def compute_coefficients_table(model, X_encoded, y, vif_table=None) -> pd.DataFr """ + try: + import statsmodels.api as sm + except ImportError as e: + raise ImportError( + "compute_coefficients_table requires statsmodels. Install with: pip install 'spotoptim[stats]'" + ) from e # Full-model R^2 and residual df r2_full = model.rsquared @@ -1065,6 +1092,12 @@ def preprocess_df_for_ols(df, independent_var_columns, target_col) -> tuple: y (pd.Series): Target variable. """ + try: + import statsmodels.api as sm + except ImportError as e: + raise ImportError( + "preprocess_df_for_ols requires statsmodels. Install with: pip install 'spotoptim[stats]'" + ) from e # Ensure the target column is numeric and 1D y = pd.to_numeric(df[target_col], errors="coerce").fillna(0).squeeze() if y.ndim != 1: diff --git a/src/spotoptim/utils/tensorboard.py b/src/spotoptim/utils/tensorboard.py index e4eddaf5..c375b49c 100644 --- a/src/spotoptim/utils/tensorboard.py +++ b/src/spotoptim/utils/tensorboard.py @@ -84,10 +84,16 @@ def init_tensorboard_writer(optimizer: SpotOptimProtocol) -> None: Args: optimizer: SpotOptim instance. """ - from torch.utils.tensorboard import SummaryWriter from datetime import datetime if optimizer.tensorboard_log: + try: + from torch.utils.tensorboard import SummaryWriter + except ImportError as e: + raise ImportError( + "TensorBoard logging requires PyTorch and tensorboard. " + "Install with: pip install 'spotoptim[torch]'" + ) from e if optimizer.tensorboard_path is None: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") optimizer.tensorboard_path = f"runs/spotoptim_{timestamp}" diff --git a/tests/conftest.py b/tests/conftest.py index 49287118..1abf65c7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -47,11 +47,7 @@ # --- whole files (every test is a full optimization run) --- "tests/test_x0_starting_point.py", # --- integration test classes --- - "tests/test_batch_eval.py::TestBatchEvalEndToEnd", "tests/test_termination_criteria.py::TestTerminationCriteria", - "tests/test_thread_pool_search.py::TestHybridExecutorEndToEnd", - "tests/test_no_gil_awareness.py::TestSimulatedNoGilPath", - "tests/test_no_gil_awareness.py::TestGilBuildEndToEnd", "tests/test_multiobjective.py::TestMultiObjectiveOptimization", "tests/test_transformations.py::TestTransformationOptimization", "tests/test_reproducibility_comprehensive.py::TestSpotOptimReproducibility", @@ -61,7 +57,6 @@ "tests/test_objective_remote.py::test_objective_remote", # --- individual heavy tests --- "tests/test_early_stopping.py::test_max_restarts_does_not_trigger_with_improvement", - "tests/test_parallel_optimization.py::TestParallelOptimization::test_parallel_execution_basic", "tests/test_multiobjective.py::TestMultiObjectiveEdgeCases::test_many_objectives", "tests/test_initial_design_nan_handling.py::test_initial_design_with_mixed_nan_inf", "tests/test_cookbook_examples.py::test_example_4_nelder_mead", @@ -77,8 +72,6 @@ "tests/test_tolerance_x.py::TestToleranceXFloatVariables::test_no_duplicate_evaluations_float", "tests/test_tolerance_x.py::TestToleranceXFactorVariables::test_no_duplicate_evaluations_factors", "tests/test_spotoptim.py::TestSpotOptimOptimize::test_optimize_with_seed_reproducibility", - "tests/test_parallel_reporting.py::test_parallel_reporting", - "tests/test_parallel_merging.py::test_parallel_merging", "tests/test_deterministic.py::TestDeterministicBehavior::test_deterministic_with_provided_initial_design", "tests/test_acquisition_failure.py::TestAcquisitionFailureWithVariableTypes::test_acquisition_failure_with_mixed_variables", "tests/test_spot_optim_args.py::test_kwargs_only", diff --git a/tests/fixtures/sequential_golden.json b/tests/fixtures/sequential_golden.json new file mode 100644 index 00000000..d546422e --- /dev/null +++ b/tests/fixtures/sequential_golden.json @@ -0,0 +1,305 @@ +{ + "num2d_sphere_seed42": { + "kwargs": { + "bounds": [ + [ + -5.0, + 5.0 + ], + [ + -5.0, + 5.0 + ] + ], + "fun": "sphere", + "max_iter": 20, + "n_initial": 8, + "seed": 42, + "verbose": false + }, + "result": { + "X": [ + [ + -1.1459301969436355, + 3.861266665457096 + ], + [ + 2.6542593692376926, + 2.1133519879823197 + ], + [ + -3.6933200930222454, + -3.968868224442189 + ], + [ + -2.495785174231741, + 0.30995815247148073 + ], + [ + 4.812023519531158, + -2.9791775601942607 + ], + [ + 1.7758780673649728, + -1.4693230108790067 + ], + [ + 0.9382960349445728, + 2.5901297800204803 + ], + [ + -4.313049228570119, + -0.10062250707487763 + ], + [ + 1.7648560258545019, + -1.4611798516658383 + ], + [ + -0.13597269653419222, + 0.1130951756429762 + ], + [ + -0.06620027285132392, + -0.029559701941424568 + ], + [ + -0.03643693055078856, + -0.018853005338895612 + ], + [ + -0.0007645246779153236, + -0.0007693139684927797 + ], + [ + -0.0002030446744782921, + -0.0003510361532865769 + ], + [ + -0.00012658739452942047, + -0.00016366698042169503 + ], + [ + -0.00016711402531233155, + -0.00021986523404349256 + ], + [ + -0.0001607517782037382, + -4.4349337909310727e-05 + ], + [ + -0.0002617493052731179, + -3.859343340906207e-05 + ], + [ + -0.0002482970289247044, + -8.524096223427868e-05 + ], + [ + -0.0003382277928359878, + -0.00011268503881056713 + ] + ], + "fun": 2.7807997968658066e-08, + "nfev": 20, + "nit": 12, + "success": true, + "x": [ + -0.0001607517782037382, + -4.4349337909310727e-05 + ], + "y": [ + 16.222536278037442, + 11.511349424294895, + 29.392528292508743, + 6.325017692198497, + 32.03106928768606, + 5.312653020446501, + 7.5891717265416485, + 18.61251853699933, + 5.249763350909147, + 0.03127909295649517, + 0.005256252104455594, + 0.001683085718271415, + 1.176341965259639e-06, + 1.6445352074823273e-07, + 4.281124893410265e-08, + 7.626781859709035e-08, + 2.7807997968658066e-08, + 7.000215191325958e-08, + 6.891743621546123e-08, + 1.270959578184429e-07 + ] + } + }, + "num3d_rosenbrock_seed7": { + "kwargs": { + "bounds": [ + [ + -2.0, + 2.0 + ], + [ + -2.0, + 2.0 + ], + [ + -2.0, + 2.0 + ] + ], + "fun": "rosenbrock", + "max_iter": 22, + "n_initial": 10, + "seed": 7, + "verbose": false + }, + "result": { + "X": [ + [ + 0.08085632526265751, + 1.5787624466974384, + 0.5634595530280415 + ], + [ + -0.34753005734009434, + -1.0917358667504984, + -0.8676644352016105 + ], + [ + 1.1646830368885834, + -1.8924104974053426, + 1.5757117353150996 + ], + [ + 1.876276949689462, + 0.18454028897165475, + -1.5412230547701729 + ], + [ + 0.6129821783158329, + 0.8386109711516214, + 0.16820048921822162 + ], + [ + 1.354668305058425, + -0.2708809442464002, + -0.24498768999018172 + ], + [ + -1.4803364514775756, + -0.49698264977973494, + -1.7696504707253273 + ], + [ + -1.0543030230308366, + -1.3571285607678263, + 1.9423097066094868 + ], + [ + -0.4098329636849676, + 1.6955295273216144, + -0.773091358634044 + ], + [ + -1.9463778673839847, + 0.5895002229817119, + 0.9980885099102617 + ], + [ + 0.6192163413774672, + 0.8321931081640853, + 0.17831488603650136 + ], + [ + 0.6830789176249513, + 0.7512116004051655, + 0.22132056414911397 + ], + [ + 0.751294119523068, + 0.6048275702692874, + 0.19331953730423476 + ], + [ + 0.7349268442006582, + 0.5577785627805486, + 0.21407232640992957 + ], + [ + 0.7140185026247633, + 0.5319076846527453, + 0.23587377091485773 + ], + [ + 0.7225635405443912, + 0.5305845510677275, + 0.2553580996163348 + ], + [ + 0.7259874591100319, + 0.5330276384423328, + 0.2582340301936535 + ], + [ + 0.7266322445636471, + 0.5364356193725648, + 0.2600151310350296 + ], + [ + 0.7305171535964367, + 0.5322524427061935, + 0.25754366952511504 + ], + [ + 0.729686761713503, + 0.5308566655036393, + 0.2568726090350779 + ], + [ + 0.7304873417053996, + 0.5315804993252615, + 0.25791120156514946 + ], + [ + 0.7302428230077695, + 0.5308809392451113, + 0.2577113012984511 + ] + ], + "fun": 0.35159826227245017, + "nfev": 22, + "nit": 12, + "success": true, + "x": [ + 0.7302428230077695, + 0.5308809392451113, + 0.2577113012984511 + ], + "y": [ + 620.4850217465407, + 577.3852704673366, + 1466.131712100796, + 1362.3891048961561, + 50.229883554132655, + 455.4031190419706, + 1137.815593900662, + 620.2262630330501, + 1566.5433259323577, + 1074.4623605775334, + 46.75538781560858, + 20.027676347149956, + 3.3566248054167476, + 1.2387804237007647, + 0.5710609176500502, + 0.37296821299958344, + 0.363710355077272, + 0.37374263869179375, + 0.35790665129665544, + 0.3555976464533338, + 0.3533107444517033, + 0.35159826227245017 + ] + } + } +} \ No newline at end of file diff --git a/tests/test_batch_eval.py b/tests/test_batch_eval.py deleted file mode 100644 index d2beaa9e..00000000 --- a/tests/test_batch_eval.py +++ /dev/null @@ -1,318 +0,0 @@ -# SPDX-FileCopyrightText: 2026 bartzbeielstein -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -"""Tests for Release 0.9.0 — Improvement F: Batch Evaluation API. - -Verifies that: -- eval_batch_size parameter is validated and stored correctly. -- remote_batch_eval_wrapper evaluates X_batch in a single fun call. -- eval_batch_size=1 (default) preserves single-point-per-call behavior. -- eval_batch_size>1 batches candidates and calls fun once per batch. -- Budget is respected regardless of batch size. -- End-to-end optimization succeeds for all batch sizes. -- eval_batch_size is ignored (n_jobs=1 path is sequential). -""" - -import numpy as np -import pytest -import dill -from spotoptim import SpotOptim -from spotoptim.utils.parallel import remote_batch_eval_wrapper - -# --------------------------------------------------------------------------- -# Shared fixtures -# --------------------------------------------------------------------------- - -BOUNDS = [(-5, 5), (-5, 5)] - - -def sphere(X): - X = np.atleast_2d(X) - return np.sum(X**2, axis=1) - - -def counting_sphere(X): - """Sphere that records every call shape for inspection.""" - X = np.atleast_2d(X) - counting_sphere.calls.append(X.shape) - return np.sum(X**2, axis=1) - - -counting_sphere.calls = [] - - -# --------------------------------------------------------------------------- -# Unit: remote_batch_eval_wrapper -# --------------------------------------------------------------------------- - - -class TestRemoteBatchEvalWrapper: - """Unit tests for the remote_batch_eval_wrapper helper.""" - - def test_single_point_batch(self): - opt = SpotOptim(fun=sphere, bounds=BOUNDS, n_initial=3, max_iter=6) - X_batch = np.array([[1.0, 2.0]]) - pickled = dill.dumps((opt, X_batch)) - X_out, y_out = remote_batch_eval_wrapper(pickled) - assert X_out.shape == (1, 2) - assert y_out.shape == (1,) - assert np.isclose(y_out[0], 5.0) - - def test_multi_point_batch(self): - opt = SpotOptim(fun=sphere, bounds=BOUNDS, n_initial=3, max_iter=6) - X_batch = np.array([[1.0, 0.0], [0.0, 2.0], [-1.0, -1.0]]) - pickled = dill.dumps((opt, X_batch)) - X_out, y_out = remote_batch_eval_wrapper(pickled) - assert X_out.shape == (3, 2) - assert y_out.shape == (3,) - assert np.allclose(y_out, [1.0, 4.0, 2.0]) - - def test_lambda_objective_serialized(self): - """Lambda functions must survive dill serialization inside the wrapper.""" - opt = SpotOptim( - fun=lambda X: np.sum(X**2, axis=1) + 1.0, - bounds=BOUNDS, - n_initial=3, - max_iter=6, - ) - X_batch = np.zeros((2, 2)) - pickled = dill.dumps((opt, X_batch)) - X_out, y_out = remote_batch_eval_wrapper(pickled) - assert np.allclose(y_out, [1.0, 1.0]) - - def test_returns_none_exception_on_failure(self): - """Wrapper must catch exceptions and return (None, exception).""" - - def bad_fun(X): - raise RuntimeError("boom") - - opt = SpotOptim(fun=bad_fun, bounds=BOUNDS, n_initial=3, max_iter=6) - X_batch = np.zeros((1, 2)) - pickled = dill.dumps((opt, X_batch)) - X_out, y_out = remote_batch_eval_wrapper(pickled) - assert X_out is None - assert isinstance(y_out, Exception) - - def test_output_shapes_preserved(self): - """X_out must equal X_batch (not a copy that drops rows).""" - opt = SpotOptim(fun=sphere, bounds=BOUNDS, n_initial=3, max_iter=6) - X_batch = np.random.default_rng(0).uniform(-5, 5, (5, 2)) - pickled = dill.dumps((opt, X_batch)) - X_out, y_out = remote_batch_eval_wrapper(pickled) - assert np.array_equal(X_out, X_batch) - assert y_out.shape == (5,) - - -# --------------------------------------------------------------------------- -# Unit: eval_batch_size parameter validation -# --------------------------------------------------------------------------- - - -class TestEvalBatchSizeValidation: - def test_default_is_one(self): - opt = SpotOptim(fun=sphere, bounds=BOUNDS, n_initial=3, max_iter=6) - assert opt.eval_batch_size == 1 - assert opt.config.eval_batch_size == 1 - - def test_positive_value_stored(self): - opt = SpotOptim( - fun=sphere, bounds=BOUNDS, n_initial=3, max_iter=6, eval_batch_size=4 - ) - assert opt.eval_batch_size == 4 - assert opt.config.eval_batch_size == 4 - - def test_zero_raises(self): - with pytest.raises(ValueError, match="eval_batch_size"): - SpotOptim( - fun=sphere, bounds=BOUNDS, n_initial=3, max_iter=6, eval_batch_size=0 - ) - - def test_negative_raises(self): - with pytest.raises(ValueError, match="eval_batch_size"): - SpotOptim( - fun=sphere, bounds=BOUNDS, n_initial=3, max_iter=6, eval_batch_size=-1 - ) - - def test_large_batch_size_stored(self): - opt = SpotOptim( - fun=sphere, bounds=BOUNDS, n_initial=3, max_iter=6, eval_batch_size=100 - ) - assert opt.eval_batch_size == 100 - - -# --------------------------------------------------------------------------- -# Integration: end-to-end with various batch sizes -# --------------------------------------------------------------------------- - - -class TestBatchEvalEndToEnd: - """Smoke tests: eval_batch_size produces correct results end-to-end.""" - - def test_batch_size_1_default(self): - """eval_batch_size=1 (default) produces a valid result.""" - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=6, - max_iter=12, - n_jobs=2, - eval_batch_size=1, - seed=0, - ) - result = opt.optimize() - assert result.success - assert result.fun < 50.0 - - def test_batch_size_2(self): - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=6, - max_iter=14, - n_jobs=2, - eval_batch_size=2, - seed=1, - ) - result = opt.optimize() - assert result.success - assert result.fun < 50.0 - - def test_batch_size_equals_n_jobs(self): - """Canonical use-case from the strategy doc: eval_batch_size=n_jobs.""" - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=6, - max_iter=16, - n_jobs=3, - eval_batch_size=3, - seed=42, - ) - result = opt.optimize() - assert result.success - assert result.fun < 50.0 - - def test_batch_size_larger_than_n_jobs(self): - """eval_batch_size > n_jobs is valid; batch fills before dispatching.""" - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=6, - max_iter=18, - n_jobs=2, - eval_batch_size=4, - seed=7, - ) - result = opt.optimize() - assert result.success - - def test_budget_respected_with_batching(self): - """Total evaluations must not exceed max_iter regardless of batch size.""" - max_iter = 14 - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=6, - max_iter=max_iter, - n_jobs=2, - eval_batch_size=3, - seed=5, - ) - result = opt.optimize() - assert result.nfev <= max_iter - - def test_result_structure_consistent_across_batch_sizes(self): - """All batch sizes produce results with the same output shape.""" - shapes = [] - for bs in [1, 2, 3]: - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=6, - max_iter=12, - n_jobs=2, - eval_batch_size=bs, - seed=0, - ) - r = opt.optimize() - assert r.success - shapes.append(r.X.shape[1]) - assert len(set(shapes)) == 1 # all same dimensionality - - def test_lambda_objective_with_batching(self): - """Lambda functions survive dill serialization in batch eval path.""" - opt = SpotOptim( - fun=lambda X: np.sum(X**2, axis=1), - bounds=BOUNDS, - n_initial=5, - max_iter=12, - n_jobs=2, - eval_batch_size=2, - seed=99, - ) - result = opt.optimize() - assert result.success - - def test_closure_objective_with_batching(self): - """Closures survive dill serialization in batch eval path.""" - scale = 3.0 - - def scaled(X): - return scale * np.sum(X**2, axis=1) - - opt = SpotOptim( - fun=scaled, - bounds=BOUNDS, - n_initial=5, - max_iter=12, - n_jobs=2, - eval_batch_size=2, - seed=11, - ) - result = opt.optimize() - assert result.success - - def test_sequential_path_ignores_eval_batch_size(self): - """n_jobs=1 (sequential path) does not crash with eval_batch_size>1.""" - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=5, - max_iter=10, - n_jobs=1, - eval_batch_size=4, - seed=0, - ) - result = opt.optimize() - assert result.success - - def test_4d_sphere_batch(self): - """Batching works for higher-dimensional problems.""" - opt = SpotOptim( - fun=sphere, - bounds=[(-3, 3)] * 4, - n_initial=8, - max_iter=16, - n_jobs=2, - eval_batch_size=2, - seed=88, - ) - result = opt.optimize() - assert result.success - assert result.X.shape[1] == 4 - - def test_minus_one_n_jobs_with_batching(self): - """n_jobs=-1 combined with eval_batch_size>1 runs without error.""" - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=5, - max_iter=12, - n_jobs=-1, - eval_batch_size=2, - seed=3, - ) - assert opt.n_jobs >= 1 - result = opt.optimize() - assert result.success diff --git a/tests/test_cookbook_examples.py b/tests/test_cookbook_examples.py index 3d6ae86a..5141b700 100644 --- a/tests/test_cookbook_examples.py +++ b/tests/test_cookbook_examples.py @@ -125,7 +125,6 @@ def test_robot_arm_scenario(): acquisition_optimizer="de_tricands", repeats_initial=1, repeats_surrogate=1, - n_jobs=2, # Parallel, but slightly fewer workers ) opt.optimize() diff --git a/tests/test_core_import_lean.py b/tests/test_core_import_lean.py new file mode 100644 index 00000000..42bf9144 --- /dev/null +++ b/tests/test_core_import_lean.py @@ -0,0 +1,63 @@ +# SPDX-FileCopyrightText: 2026 bartzbeielstein +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Regression gate: verify that importing spotoptim and running a Kriging optimization +does NOT eagerly load torch, matplotlib, requests, statsmodels, or seaborn. + +Runs the actual assertion in a subprocess so that the test environment's +pre-imported modules do not mask eager-import regressions (even though all +extras ARE installed in the dev environment). +""" + +import subprocess +import sys + +_SUBPROCESS_CODE = """ +import sys + +import spotoptim +from spotoptim import SpotOptim, Kriging +from spotoptim.function import sphere + +# Run a tiny Kriging optimization using the real public API +result = SpotOptim( + fun=sphere, + bounds=[(-5, 5), (-5, 5)], + max_iter=4, + n_initial=3, + seed=42, +).optimize() + +# Assert no heavy optional dependencies were pulled in +FORBIDDEN = ("torch", "matplotlib", "requests", "statsmodels", "seaborn") +leaked = [name for name in FORBIDDEN if name in sys.modules] +if leaked: + loaded = sorted(k for k in sys.modules if any(k == f or k.startswith(f + ".") for f in FORBIDDEN)) + print("LEAKED MODULES:", loaded, file=sys.stderr) + sys.exit(1) + +print("lean import OK") +print(f"result.fun = {result.fun}") +""" + + +def test_core_import_is_lean(): + """Import spotoptim and run a kriging optimization in a clean subprocess. + + Asserts that none of torch, matplotlib, requests, statsmodels, or seaborn + appear in sys.modules after the import and optimization run. + """ + proc = subprocess.run( + [sys.executable, "-c", _SUBPROCESS_CODE], + capture_output=True, + text=True, + ) + assert proc.returncode == 0, ( + f"Lean-import smoke test FAILED.\n" + f"stdout: {proc.stdout}\n" + f"stderr: {proc.stderr}" + ) + assert ( + "lean import OK" in proc.stdout + ), f"Expected 'lean import OK' in subprocess output, got:\n{proc.stdout}" diff --git a/tests/test_execute_optimization_run_example.py b/tests/test_execute_optimization_run_example.py index 65e36790..05102881 100644 --- a/tests/test_execute_optimization_run_example.py +++ b/tests/test_execute_optimization_run_example.py @@ -14,7 +14,6 @@ def testexecute_optimization_run_example(): n_initial=5, max_iter=10, seed=0, - n_jobs=1, verbose=False, ) diff --git a/tests/test_int_log10_dims.py b/tests/test_int_log10_dims.py index 77b4a138..60728735 100644 --- a/tests/test_int_log10_dims.py +++ b/tests/test_int_log10_dims.py @@ -127,14 +127,6 @@ def test_history_and_best_respect_bounds(self): assert LOW <= opt.best_x_[0] <= HIGH assert opt.best_x_[0] == round(opt.best_x_[0]) - def test_parallel_path_respects_bounds(self): - seen = [] - opt = _make_optimizer(seen, n_jobs=2, max_iter=30, n_initial=15) - opt.optimize() - col = np.asarray(opt.X_)[:, 0] - assert np.all(col >= LOW) and np.all(col <= HIGH) - np.testing.assert_array_equal(col, np.around(col)) - class TestRepairNaturalX: """Unit behaviour of the natural-scale repair.""" diff --git a/tests/test_n_jobs_convention.py b/tests/test_n_jobs_convention.py deleted file mode 100644 index 6118bae2..00000000 --- a/tests/test_n_jobs_convention.py +++ /dev/null @@ -1,108 +0,0 @@ -# SPDX-FileCopyrightText: 2026 bartzbeielstein -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -"""Tests for the n_jobs=-1 convention (release 0.7.0, Improvement E).""" - -import os -import pytest -import numpy as np -from spotoptim import SpotOptim - - -def sphere(X): - X = np.atleast_2d(X) - return np.sum(X**2, axis=1) - - -BOUNDS = [(-5, 5), (-5, 5)] - - -class TestNJobsResolution: - """Unit tests for n_jobs validation and -1 resolution.""" - - def test_default_is_sequential(self): - opt = SpotOptim(fun=sphere, bounds=BOUNDS, n_initial=3, max_iter=5) - assert opt.n_jobs == 1 - - def test_positive_integer_unchanged(self): - opt = SpotOptim(fun=sphere, bounds=BOUNDS, n_initial=3, max_iter=5, n_jobs=2) - assert opt.n_jobs == 2 - - def test_minus_one_resolves_to_cpu_count(self): - opt = SpotOptim(fun=sphere, bounds=BOUNDS, n_initial=3, max_iter=5, n_jobs=-1) - expected = os.cpu_count() or 1 - assert opt.n_jobs == expected - - def test_minus_one_resolves_to_positive(self): - opt = SpotOptim(fun=sphere, bounds=BOUNDS, n_initial=3, max_iter=5, n_jobs=-1) - assert opt.n_jobs >= 1 - - # --- invalid values raise ValueError --- - - def test_zero_raises(self): - with pytest.raises(ValueError, match="n_jobs"): - SpotOptim(fun=sphere, bounds=BOUNDS, n_initial=3, max_iter=5, n_jobs=0) - - def test_minus_two_raises(self): - with pytest.raises(ValueError, match="n_jobs"): - SpotOptim(fun=sphere, bounds=BOUNDS, n_initial=3, max_iter=5, n_jobs=-2) - - def test_minus_ten_raises(self): - with pytest.raises(ValueError, match="n_jobs"): - SpotOptim(fun=sphere, bounds=BOUNDS, n_initial=3, max_iter=5, n_jobs=-10) - - def test_error_message_contains_value(self): - with pytest.raises(ValueError, match="-5"): - SpotOptim(fun=sphere, bounds=BOUNDS, n_initial=3, max_iter=5, n_jobs=-5) - - -class TestNJobsMinusOneOptimizes: - """Smoke tests: n_jobs=-1 actually runs optimization end-to-end.""" - - def test_sequential_fallback_single_core(self): - """When os.cpu_count()==1 (or resolved to 1), n_jobs=-1 runs sequentially.""" - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=5, - max_iter=8, - n_jobs=-1, - seed=42, - ) - # n_jobs must be a valid positive integer after resolution - assert opt.n_jobs >= 1 - result = opt.optimize() - assert result.success - assert result.nfev >= 5 - - def test_n_jobs_minus_one_same_result_structure_as_positive(self): - """n_jobs=-1 produces the same result structure as n_jobs=1.""" - opt1 = SpotOptim( - fun=sphere, bounds=BOUNDS, n_initial=5, max_iter=8, n_jobs=1, seed=0 - ) - opt_auto = SpotOptim( - fun=sphere, bounds=BOUNDS, n_initial=5, max_iter=8, n_jobs=-1, seed=0 - ) - r1 = opt1.optimize() - r_auto = opt_auto.optimize() - - assert r1.success - assert r_auto.success - assert r_auto.X.shape[1] == r1.X.shape[1] - assert r_auto.fun < 100.0 # sanity: sphere minimum is 0 - - -class TestNJobsStoredAfterResolution: - """Verify the resolved value is what the optimizer actually uses.""" - - def test_config_reflects_resolved_value(self): - opt = SpotOptim(fun=sphere, bounds=BOUNDS, n_initial=3, max_iter=5, n_jobs=-1) - # The config attribute must hold the resolved (positive) value - assert opt.config.n_jobs == opt.n_jobs - assert opt.config.n_jobs >= 1 - - def test_large_positive_n_jobs_unchanged(self): - opt = SpotOptim(fun=sphere, bounds=BOUNDS, n_initial=3, max_iter=5, n_jobs=4) - assert opt.n_jobs == 4 - assert opt.config.n_jobs == 4 diff --git a/tests/test_no_gil_awareness.py b/tests/test_no_gil_awareness.py deleted file mode 100644 index 6ca0dd06..00000000 --- a/tests/test_no_gil_awareness.py +++ /dev/null @@ -1,310 +0,0 @@ -# SPDX-FileCopyrightText: 2026 bartzbeielstein -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -"""Tests for Release 0.10.0 — Improvement D: Free-Threaded (No-GIL) Awareness. - -Verifies that: -- is_gil_disabled() returns a bool and reflects the actual GIL status. -- is_gil_disabled() returns False on standard (GIL-enabled) Python builds, - which is expected in all current CI environments. -- is_gil_disabled() handles the absence of sys.is_gil_enabled gracefully - (Python < 3.13 compatibility). -- The executor selection logic branches correctly based on GIL status: - GIL build → ProcessPoolExecutor for eval, ThreadPoolExecutor for search - No-GIL → ThreadPoolExecutor for both eval and search -- End-to-end optimization works correctly on the standard GIL build - (this is the path exercised in CI; no-GIL requires python3.13t). -- The is_gil_disabled() helper is importable from spotoptim.SpotOptim. -""" - -import sys -import importlib -import unittest.mock as mock -import numpy as np -from spotoptim import SpotOptim -from spotoptim.utils.parallel import is_gil_disabled -from spotoptim.optimizer import steady_state as _steady_mod - -# importlib bypasses the __init__.py class re-export and gives the module. -_spotoptim_mod = importlib.import_module("spotoptim.SpotOptim") - -# --------------------------------------------------------------------------- -# Shared fixtures -# --------------------------------------------------------------------------- - -BOUNDS = [(-5, 5), (-5, 5)] - - -def sphere(X): - X = np.atleast_2d(X) - return np.sum(X**2, axis=1) - - -# --------------------------------------------------------------------------- -# Unit: is_gil_disabled() -# --------------------------------------------------------------------------- - - -class TestIsGilDisabled: - """Unit tests for the is_gil_disabled() helper.""" - - def test_returns_bool(self): - result = is_gil_disabled() - assert isinstance(result, bool) - - def test_false_on_standard_build(self): - """On a standard GIL-enabled Python the function must return False.""" - # All current CI environments run standard GIL builds. - # is_gil_enabled() exists on CPython 3.13+ and returns True when GIL - # is active. On older Python the attribute is absent and the lambda - # default returns True — either way is_gil_disabled() is False. - result = is_gil_disabled() - if hasattr(sys, "is_gil_enabled"): - assert result == (not sys.is_gil_enabled()) - else: - # Python < 3.13: attribute absent → GIL assumed enabled → False - assert result is False - - def test_consistent_across_calls(self): - """GIL status does not change within a running process.""" - assert is_gil_disabled() == is_gil_disabled() - - def test_mocked_gil_disabled(self): - """Simulate a no-GIL build by mocking sys.is_gil_enabled.""" - with mock.patch.object(sys, "is_gil_enabled", return_value=False, create=True): - assert is_gil_disabled() is True - - def test_mocked_gil_enabled(self): - """Simulate a standard GIL build by mocking sys.is_gil_enabled.""" - with mock.patch.object(sys, "is_gil_enabled", return_value=True, create=True): - assert is_gil_disabled() is False - - def test_no_attribute_fallback(self): - """When sys.is_gil_enabled is absent the helper returns False.""" - # Remove attribute if it exists, then call the helper - original = getattr(sys, "is_gil_enabled", None) - if hasattr(sys, "is_gil_enabled"): - delattr(sys, "is_gil_enabled") - try: - result = is_gil_disabled() - assert result is False - finally: - if original is not None: - sys.is_gil_enabled = original - - -# --------------------------------------------------------------------------- -# Unit: executor selection logic -# --------------------------------------------------------------------------- - - -class TestExecutorSelection: - """Verify the correct executor types are chosen based on GIL status.""" - - def test_gil_build_uses_process_pool_for_eval(self): - """On a GIL build optimize_steady_state must use ProcessPoolExecutor - for eval. We verify by checking that is_gil_disabled() is False on - this interpreter, which is the precondition for that path.""" - # If GIL is enabled (standard build), the eval pool is a Process pool. - assert not is_gil_disabled(), ( - "This test must run on a standard GIL build; " - "skip it on free-threaded Python." - ) - - def test_no_gil_mock_uses_thread_pool_for_eval(self, monkeypatch): - """When GIL is mocked as disabled, is_gil_disabled() returns True, - signalling the thread-pool eval path.""" - monkeypatch.setattr(sys, "is_gil_enabled", lambda: False, raising=False) - assert is_gil_disabled() is True - - def test_process_pool_executor_importable(self): - """ProcessPoolExecutor must be importable (GIL-build eval path).""" - from concurrent.futures import ProcessPoolExecutor # noqa: F401 - - assert ProcessPoolExecutor is not None - - def test_thread_pool_executor_importable(self): - """ThreadPoolExecutor must be importable (search path + no-GIL eval).""" - from concurrent.futures import ThreadPoolExecutor # noqa: F401 - - assert ThreadPoolExecutor is not None - - -# --------------------------------------------------------------------------- -# Integration: end-to-end on standard GIL build -# --------------------------------------------------------------------------- - - -class TestGilBuildEndToEnd: - """Smoke tests on the current (GIL-enabled) Python build. - - These tests exercise the standard code path: ProcessPoolExecutor for eval, - ThreadPoolExecutor for search. They also confirm that the no-GIL detection - layer does not break anything on GIL builds. - """ - - def test_n_jobs_2_sphere(self): - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=6, - max_iter=12, - n_jobs=2, - seed=0, - ) - result = opt.optimize() - assert result.success - assert result.fun < 50.0 - - def test_n_jobs_3_sphere(self): - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=6, - max_iter=14, - n_jobs=3, - seed=7, - ) - result = opt.optimize() - assert result.success - assert result.fun < 50.0 - - def test_lambda_objective_parallel(self): - opt = SpotOptim( - fun=lambda X: np.sum(X**2, axis=1), - bounds=BOUNDS, - n_initial=5, - max_iter=10, - n_jobs=2, - seed=1, - ) - result = opt.optimize() - assert result.success - - def test_sequential_path_unaffected(self): - """n_jobs=1 must be completely unaffected by the no-GIL change.""" - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=5, - max_iter=10, - n_jobs=1, - seed=0, - ) - result = opt.optimize() - assert result.success - - def test_minus_one_n_jobs_parallel(self): - """n_jobs=-1 must resolve and run without error on a GIL build.""" - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=5, - max_iter=10, - n_jobs=-1, - seed=3, - ) - assert opt.n_jobs >= 1 - result = opt.optimize() - assert result.success - - -# --------------------------------------------------------------------------- -# Integration: simulated no-GIL path via monkeypatch -# --------------------------------------------------------------------------- - - -class TestSimulatedNoGilPath: - """Test the thread-based eval path by mocking is_gil_disabled() to True. - - These tests exercise _thread_eval_task_single and _thread_batch_eval_task - on the current interpreter by making optimize_steady_state believe it is - running on a free-threaded build. - """ - - def test_simulated_no_gil_sphere(self, monkeypatch): - """Optimization succeeds when both pools are ThreadPoolExecutor.""" - monkeypatch.setattr(_steady_mod, "is_gil_disabled", lambda: True) - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=6, - max_iter=12, - n_jobs=2, - seed=0, - ) - result = opt.optimize() - assert result.success - assert result.fun < 50.0 - - def test_simulated_no_gil_lambda(self, monkeypatch): - """Lambda objectives work in the thread-based eval path.""" - monkeypatch.setattr(_steady_mod, "is_gil_disabled", lambda: True) - opt = SpotOptim( - fun=lambda X: np.sum(X**2, axis=1), - bounds=BOUNDS, - n_initial=5, - max_iter=10, - n_jobs=2, - seed=42, - ) - result = opt.optimize() - assert result.success - - def test_simulated_no_gil_with_batch_size(self, monkeypatch): - """Batch eval + no-GIL path: _thread_batch_eval_task is used.""" - monkeypatch.setattr(_steady_mod, "is_gil_disabled", lambda: True) - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=6, - max_iter=14, - n_jobs=2, - eval_batch_size=2, - seed=5, - ) - result = opt.optimize() - assert result.success - - def test_simulated_no_gil_4d(self, monkeypatch): - """Thread-based eval path handles higher-dimensional problems.""" - monkeypatch.setattr(_steady_mod, "is_gil_disabled", lambda: True) - opt = SpotOptim( - fun=sphere, - bounds=[(-3, 3)] * 4, - n_initial=8, - max_iter=16, - n_jobs=2, - seed=88, - ) - result = opt.optimize() - assert result.success - assert result.X.shape[1] == 4 - - def test_simulated_no_gil_result_shape_matches_gil(self, monkeypatch): - """No-GIL and GIL paths produce results with the same dimensionality.""" - opt_gil = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=6, - max_iter=12, - n_jobs=2, - seed=0, - ) - r_gil = opt_gil.optimize() - - monkeypatch.setattr(_steady_mod, "is_gil_disabled", lambda: True) - opt_no_gil = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=6, - max_iter=12, - n_jobs=2, - seed=0, - ) - r_no_gil = opt_no_gil.optimize() - - assert r_gil.success - assert r_no_gil.success - assert r_no_gil.X.shape[1] == r_gil.X.shape[1] diff --git a/tests/test_no_parallel_params.py b/tests/test_no_parallel_params.py new file mode 100644 index 00000000..6733c9e6 --- /dev/null +++ b/tests/test_no_parallel_params.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2026 bartzbeielstein +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Contract test: the removed parallel parameters are hard-rejected. + +After the parallel-evaluation subsystem was removed, ``SpotOptim`` must not +accept the ``n_jobs`` or ``eval_batch_size`` constructor arguments. Passing +either should raise ``TypeError`` (Python's unknown-keyword rejection). This +test pins that contract so a future ``**kwargs`` passthrough cannot silently +re-admit the dead parameters. +""" + +import numpy as np +import pytest + +from spotoptim import SpotOptim + + +def _objective(X): + return np.sum(X**2, axis=1) + + +@pytest.mark.parametrize("param", ["n_jobs", "eval_batch_size"]) +def test_removed_parallel_param_raises_type_error(param): + """Constructing SpotOptim with a removed parallel param raises TypeError.""" + with pytest.raises(TypeError, match=param): + SpotOptim(fun=_objective, bounds=[(-5.0, 5.0), (-5.0, 5.0)], **{param: 2}) diff --git a/tests/test_optimize_sequential_run_example.py b/tests/test_optimize_sequential_run_example.py index 1ba17746..f3883f02 100644 --- a/tests/test_optimize_sequential_run_example.py +++ b/tests/test_optimize_sequential_run_example.py @@ -16,7 +16,6 @@ def testoptimize_sequential_run_example(): n_initial=5, max_iter=20, seed=0, - n_jobs=1, verbose=False, ) diff --git a/tests/test_optimize_steady_state_example.py b/tests/test_optimize_steady_state_example.py deleted file mode 100644 index 2e34c7ee..00000000 --- a/tests/test_optimize_steady_state_example.py +++ /dev/null @@ -1,27 +0,0 @@ -# SPDX-FileCopyrightText: 2026 bartzbeielstein -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -import time - -import numpy as np - -from spotoptim import SpotOptim - - -def testoptimize_steady_state_example(): - opt = SpotOptim( - fun=lambda X: np.sum(X**2, axis=1), - bounds=[(-5, 5), (-5, 5)], - n_initial=5, - max_iter=10, - seed=0, - n_jobs=2, - verbose=True, - ) - - status, result = opt.optimize_steady_state(timeout_start=time.time(), X0=None) - - assert status == "FINISHED" - assert result.message.splitlines()[0] == "Optimization finished (Steady State)" - assert result.nfev == 10 diff --git a/tests/test_parallel_merging.py b/tests/test_parallel_merging.py deleted file mode 100644 index 43a81d50..00000000 --- a/tests/test_parallel_merging.py +++ /dev/null @@ -1,57 +0,0 @@ -# SPDX-FileCopyrightText: 2026 bartzbeielstein -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -import numpy as np -from spotoptim import SpotOptim - - -def obj_fun(X): - # Simple sphere function - return np.sum(X**2, axis=1) - - -def test_parallel_merging(): - """ - Test that SpotOptim correctly merges results from parallel runs. - - 1. Verify global best is found. - 2. Verify total evaluations reflect all parallel work. - """ - - n_jobs = 2 - max_iter = 10 - n_initial = 4 - - # Run optimization - opt = SpotOptim( - fun=obj_fun, - bounds=[(-5, 5)] * 2, - max_iter=max_iter, - n_initial=n_initial, - n_jobs=n_jobs, - seed=42, - verbose=True, - ) - - opt.optimize() - - # 1. Check if best result is reasonable (should find something close to 0) - assert opt.best_y_ is not None - assert opt.best_y_ < 50.0 # Loose bound, just ensuring it ran - - # 2. Check total evaluations - # In steady-state parallelization, max_iter is the global budget. - expected_evals = max_iter - - print(f"Reported evaluations (opt.counter): {opt.counter}") - print(f"Expected evaluations (n_jobs * max_iter): {expected_evals}") - - # Assert that we have recorded data for ALL evaluations, not just the best run - assert ( - opt.counter >= expected_evals * 0.9 - ), f"Evaluations count {opt.counter} is significantly less than expected {expected_evals}. Data from parallel runs might be lost." - - # Also check underlying data arrays - assert len(opt.y_) == opt.counter - assert len(opt.X_) == opt.counter diff --git a/tests/test_parallel_optimization.py b/tests/test_parallel_optimization.py deleted file mode 100644 index dd7a20bc..00000000 --- a/tests/test_parallel_optimization.py +++ /dev/null @@ -1,98 +0,0 @@ -# SPDX-FileCopyrightText: 2026 bartzbeielstein -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -import pytest -import numpy as np -from spotoptim.SpotOptim import SpotOptim -from scipy.optimize import OptimizeResult - - -class TestParallelOptimization: - """Test suite for parallel optimization using n_jobs.""" - - def test_parallel_execution_basic(self): - """Test simple parallel execution with n_jobs=2.""" - - def sphere(X): - X = np.atleast_2d(X) - return np.sum(X**2, axis=1) - - bounds = [(-5, 5), (-5, 5)] - - # Ensure enough budget for multiple runs - # n_initial=5, max_iter=20. - # If run 1 consumes 20, loop checks budget. - # But parallel batch launches 2. - # Both get remaining_iter=20. - # Total evals will likely be ~40 if full runs happen? - # Actually restarts happen only if needed. - # For sphere, it might succeed quickly. - - opt = SpotOptim( - fun=sphere, - bounds=bounds, - n_initial=5, - max_iter=30, # Generous budget - n_jobs=2, - seed=42, - verbose=True, - ) - - result = opt.optimize() - assert isinstance(result, OptimizeResult) - assert result.success is True - # In steady-state, we do one optimized run using multiple workers. - # So restarts_results_ will typically have 1 result unless restarts are triggered. - assert len(opt.restarts_results_) >= 1 - - def test_parallel_seeds_diversity(self): - """Test irrelevant for steady-state (single run).""" - pass - - def test_parallel_budget_exhaustion(self): - """Test that optimization stops when global budget is exhausted.""" - - def sphere(X): - X = np.atleast_2d(X) - return np.sum(X**2, axis=1) - - bounds = [(-5, 5)] - - # Small budget, large n_jobs - # max_iter=8, n_initial=3. - # n_jobs=4. - # It should launch 4 tasks. - # Each sees remaining=8. - # Each runs 3 initial + maybe more? - # Total evals will be higher than max_iter likely, but that's expected with parallel oversubscription. - - opt = SpotOptim( - fun=sphere, - bounds=bounds, - n_initial=3, - max_iter=8, - n_jobs=4, - seed=42, - verbose=False, - ) - - _ = opt.optimize() - - # Check total evaluations - total_evals = sum(len(r.y) for r in opt.restarts_results_) - - # In steady state, max_iter = 8 is the global budget. - # We expect around 8 evaluations. - assert total_evals >= 8 - - # Should not launch another batch because budget is definitely gone - # The loop check `remaining_iter < n_initial` will catch this. - - def test_parallel_pickling_check(self): - """Implicitly tests pickling since joblib requires it.""" - pass - - -if __name__ == "__main__": - pytest.main([__file__]) diff --git a/tests/test_parallel_reporting.py b/tests/test_parallel_reporting.py deleted file mode 100644 index 9a0afe39..00000000 --- a/tests/test_parallel_reporting.py +++ /dev/null @@ -1,53 +0,0 @@ -# SPDX-FileCopyrightText: 2026 bartzbeielstein -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -import numpy as np -from spotoptim import SpotOptim - - -def dummy_func(X): - return np.sum(X**2, axis=1) - - -def test_parallel_reporting(capsys): - """ - Verify that GlobalBest column appears in output when running in parallel. - We'll assume the output is captured by capsys. - """ - n_jobs = 2 - max_iter = 10 - - opt = SpotOptim( - fun=dummy_func, - bounds=[(-5.0, 5.0)] * 2, - max_iter=max_iter, - n_initial=4, - n_jobs=n_jobs, - verbose=1, - seed=42, # Ensure reproducibility - ) - - res = opt.optimize() - - captured = capsys.readouterr() - output = captured.out - - print("Captured Output:\n", output) - - # Note: capturing stdout from joblib subprocesses with capsys is unreliable. - # We rely on manual verification for the "GlobalBest" string presence. - # This test mainly ensures that running with parallel reporting enabled doesn't crash. - # assert "GlobalBest" in output, "Output should contain 'GlobalBest' column in parallel mode" - - # We can also check if valid values are printed - # e.g. "GlobalBest: 0.123456" - import re - - _ = re.findall(r"GlobalBest: \d+\.\d+", output) - if "Iter" in output: # If any iterations were printed - pass # It's enough that the column header/value appeared. - - # Ensure optimization still works - assert res.success - assert opt.counter > 0 diff --git a/tests/test_refactored_optimize.py b/tests/test_refactored_optimize.py index 01102478..4d66c470 100644 --- a/tests/test_refactored_optimize.py +++ b/tests/test_refactored_optimize.py @@ -25,15 +25,12 @@ def sphere(X): bounds=[(-5, 5), (-5, 5)], n_initial=5, max_iter=10, - n_jobs=1, seed=42, verbose=False, ) def test_optimize_structure_sequential(self, spot_optim): """Test that optimize calls execute_optimization_run and loop works for sequential.""" - spot_optim.n_jobs = 1 - # Mock execute_optimization_run to return a finished result immediately mock_result = OptimizeResult( x=np.zeros(2), @@ -59,7 +56,6 @@ def test_optimize_structure_sequential(self, spot_optim): def test_optimize_structure_restart(self, spot_optim): """Test restart logic in optimize loop.""" - spot_optim.n_jobs = 1 spot_optim.max_iter = 20 spot_optim.n_initial = 5 spot_optim.restart_inject_best = True @@ -101,30 +97,14 @@ def test_optimize_structure_restart(self, spot_optim): assert args2[1]["y0_known"] == 1.0 def test_dispatch_sequential(self, spot_optim): - """Verify dispatch calls optimize_sequential_run when n_jobs=1.""" - spot_optim.n_jobs = 1 + """Verify execute_optimization_run always calls optimize_sequential_run.""" spot_optim.optimize_sequential_run = MagicMock( return_value=("FINISHED", MagicMock()) ) - spot_optim.optimize_steady_state = MagicMock() spot_optim.execute_optimization_run(timeout_start=time.time()) spot_optim.optimize_sequential_run.assert_called_once() - spot_optim.optimize_steady_state.assert_not_called() - - def test_dispatch_parallel(self, spot_optim): - """Verify dispatch calls optimize_steady_state when n_jobs>1.""" - spot_optim.n_jobs = 2 - spot_optim.optimize_sequential_run = MagicMock() - spot_optim.optimize_steady_state = MagicMock( - return_value=("FINISHED", MagicMock()) - ) - - spot_optim.execute_optimization_run(timeout_start=time.time()) - - spot_optim.optimize_steady_state.assert_called_once() - spot_optim.optimize_sequential_run.assert_not_called() def test_initialize_run_calls(self, spot_optim): """Verify _initialize_run calls necessary setup methods.""" diff --git a/tests/test_remote_eval_wrapper_example.py b/tests/test_remote_eval_wrapper_example.py deleted file mode 100644 index c06bfcca..00000000 --- a/tests/test_remote_eval_wrapper_example.py +++ /dev/null @@ -1,140 +0,0 @@ -# SPDX-FileCopyrightText: 2026 bartzbeielstein -# -# SPDX-License-Identifier: AGPL-3.0-or-later - - -import numpy as np -import dill - -from spotoptim.utils.parallel import remote_eval_wrapper - - -class DummyOptimizer: - """Simple optimizer for testing purposes.""" - - def evaluate_function(self, X): - """Evaluate sum of squares.""" - return np.sum(X**2, axis=1) - - -class FailingOptimizer: - """Optimizer that always raises an error.""" - - def evaluate_function(self, X): - raise ValueError("Intentional evaluation error") - - -def test_remote_eval_wrapper_example(): - """Test remote_eval_wrapper with basic example from documentation.""" - optimizer = DummyOptimizer() - x = np.array([1.0, 2.0]) - - pickled_args = dill.dumps((optimizer, x)) - x_eval, y_eval = remote_eval_wrapper(pickled_args) - - assert np.allclose(x_eval, x) - assert np.isclose(y_eval, 5.0) # 1^2 + 2^2 = 5 - - -def test_remote_eval_wrapper_single_dimension(): - """Test remote_eval_wrapper with 1D input.""" - optimizer = DummyOptimizer() - x = np.array([3.0]) - - pickled_args = dill.dumps((optimizer, x)) - x_eval, y_eval = remote_eval_wrapper(pickled_args) - - assert np.allclose(x_eval, x) - assert np.isclose(y_eval, 9.0) # 3^2 = 9 - - -def test_remote_eval_wrapper_high_dimensional(): - """Test remote_eval_wrapper with high-dimensional input.""" - optimizer = DummyOptimizer() - x = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) - - pickled_args = dill.dumps((optimizer, x)) - x_eval, y_eval = remote_eval_wrapper(pickled_args) - - assert np.allclose(x_eval, x) - # 1 + 4 + 9 + 16 + 25 = 55 - assert np.isclose(y_eval, 55.0) - - -def test_remote_eval_wrapper_zero_point(): - """Test remote_eval_wrapper at the origin.""" - optimizer = DummyOptimizer() - x = np.array([0.0, 0.0, 0.0]) - - pickled_args = dill.dumps((optimizer, x)) - x_eval, y_eval = remote_eval_wrapper(pickled_args) - - assert np.allclose(x_eval, x) - assert np.isclose(y_eval, 0.0) - - -def test_remote_eval_wrapper_negative_values(): - """Test remote_eval_wrapper with negative values.""" - optimizer = DummyOptimizer() - x = np.array([-2.0, -3.0]) - - pickled_args = dill.dumps((optimizer, x)) - x_eval, y_eval = remote_eval_wrapper(pickled_args) - - assert np.allclose(x_eval, x) - # (-2)^2 + (-3)^2 = 4 + 9 = 13 - assert np.isclose(y_eval, 13.0) - - -def test_remote_eval_wrapper_error_handling(): - """Test remote_eval_wrapper handles evaluation errors correctly.""" - optimizer = FailingOptimizer() - x = np.array([1.0, 2.0]) - - pickled_args = dill.dumps((optimizer, x)) - x_eval, y_eval = remote_eval_wrapper(pickled_args) - - # When an exception occurs, x_eval should be None - assert x_eval is None - # y_eval should be the exception - assert isinstance(y_eval, Exception) - assert isinstance(y_eval, ValueError) - assert "Intentional evaluation error" in str(y_eval) - - -def test_remote_eval_wrapper_with_custom_function(): - """Test remote_eval_wrapper with a different objective function.""" - - class RosenbrockOptimizer: - """Optimizer using Rosenbrock function.""" - - def evaluate_function(self, X): - """Rosenbrock function: (1-x)^2 + 100*(y-x^2)^2""" - x = X[:, 0] - y = X[:, 1] - return (1 - x) ** 2 + 100 * (y - x**2) ** 2 - - optimizer = RosenbrockOptimizer() - x = np.array([0.0, 0.0]) - - pickled_args = dill.dumps((optimizer, x)) - x_eval, y_eval = remote_eval_wrapper(pickled_args) - - assert np.allclose(x_eval, x) - # At (0,0): (1-0)^2 + 100*(0-0)^2 = 1 - assert np.isclose(y_eval, 1.0) - - -def test_remote_eval_wrapper_preserves_input(): - """Test that remote_eval_wrapper preserves the input point.""" - optimizer = DummyOptimizer() - original_x = np.array([1.5, 2.5, 3.5]) - x = original_x.copy() - - pickled_args = dill.dumps((optimizer, x)) - x_eval, y_eval = remote_eval_wrapper(pickled_args) - - # Verify the returned x matches the input - assert np.array_equal(x_eval, original_x) - # Verify original array wasn't modified - assert np.array_equal(x, original_x) diff --git a/tests/test_remote_search_task_example.py b/tests/test_remote_search_task_example.py deleted file mode 100644 index d6b0d656..00000000 --- a/tests/test_remote_search_task_example.py +++ /dev/null @@ -1,234 +0,0 @@ -# SPDX-FileCopyrightText: 2026 bartzbeielstein -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -""" -Tests for remote_search_task function based on documentation examples. -These tests validate the parallel search functionality with dill serialization. -""" - -import numpy as np -import dill -from spotoptim.utils.parallel import remote_search_task -from spotoptim import SpotOptim - - -def test_remote_search_task_basic_example(): - """Test basic usage from documentation example.""" - opt = SpotOptim( - fun=lambda X: np.sum(X**2, axis=1), - bounds=[(-5, 5), (-5, 5)], - n_initial=5, - max_iter=10, - seed=0, - verbose=False, # Suppress output during testing - ) - - # Initialize the optimizer by manually setting data and fitting surrogate - np.random.seed(0) - opt.X_ = np.random.rand(10, 2) * 10 - 5 # Scale to bounds [-5, 5] - opt.y_ = np.sum(opt.X_**2, axis=1) - opt.fit_surrogate(opt.X_, opt.y_) - - pickled_optimizer = dill.dumps(opt) - x_new = remote_search_task(pickled_optimizer) - - # The function should return an infill point (ndarray), not an Exception - assert isinstance(x_new, np.ndarray), f"Expected ndarray, got {type(x_new)}" - assert x_new.shape == (1, 2), f"Expected shape (1, 2), got {x_new.shape}" - - # Check that the point is within bounds - for i, (low, high) in enumerate([(-5, 5), (-5, 5)]): - assert ( - low <= x_new[0, i] <= high - ), f"Point {x_new[0, i]} out of bounds [{low}, {high}]" - - -def test_remote_search_task_1d_problem(): - """Test with 1D optimization problem.""" - opt = SpotOptim( - fun=lambda X: X**2, - bounds=[(-10, 10)], - n_initial=3, - max_iter=5, - seed=42, - verbose=False, - ) - - # Initialize the optimizer by manually setting data and fitting surrogate - np.random.seed(42) - opt.X_ = np.random.rand(8, 1) * 20 - 10 # Scale to bounds [-10, 10] - opt.y_ = (opt.X_**2).ravel() - opt.fit_surrogate(opt.X_, opt.y_) - - pickled_optimizer = dill.dumps(opt) - x_new = remote_search_task(pickled_optimizer) - - assert isinstance(x_new, np.ndarray) - assert x_new.shape == (1, 1) - assert -10 <= x_new[0, 0] <= 10 - - -def test_remote_search_task_5d_problem(): - """Test with 5D optimization problem.""" - opt = SpotOptim( - fun=lambda X: np.sum(X**2, axis=1), - bounds=[(-3, 3)] * 5, - n_initial=10, - max_iter=15, - seed=123, - verbose=False, - ) - - # Initialize the optimizer by manually setting data and fitting surrogate - np.random.seed(123) - opt.X_ = np.random.rand(12, 5) * 6 - 3 # Scale to bounds [-3, 3] - opt.y_ = np.sum(opt.X_**2, axis=1) - opt.fit_surrogate(opt.X_, opt.y_) - - pickled_optimizer = dill.dumps(opt) - x_new = remote_search_task(pickled_optimizer) - - assert isinstance(x_new, np.ndarray) - assert x_new.shape == (1, 5) - - # Check all dimensions are within bounds - for i in range(5): - assert -3 <= x_new[0, i] <= 3 - - -def test_remote_search_task_custom_objective(): - """Test with custom objective function (Rosenbrock).""" - - def rosenbrock(X): - """Rosenbrock function: f(x,y) = (1-x)^2 + 100(y-x^2)^2""" - x = X[:, 0] - y = X[:, 1] - return (1 - x) ** 2 + 100 * (y - x**2) ** 2 - - opt = SpotOptim( - fun=rosenbrock, - bounds=[(-2, 2), (-2, 2)], - n_initial=6, - max_iter=12, - seed=99, - verbose=False, - ) - - # Initialize the optimizer by manually setting data and fitting surrogate - np.random.seed(99) - opt.X_ = np.random.rand(10, 2) * 4 - 2 # Scale to bounds [-2, 2] - opt.y_ = rosenbrock(opt.X_) - opt.fit_surrogate(opt.X_, opt.y_) - - pickled_optimizer = dill.dumps(opt) - x_new = remote_search_task(pickled_optimizer) - - assert isinstance(x_new, np.ndarray) - assert x_new.shape == (1, 2) - assert -2 <= x_new[0, 0] <= 2 - assert -2 <= x_new[0, 1] <= 2 - - -def test_remote_search_task_with_acquisition_ei(): - """Test with Expected Improvement acquisition function.""" - opt = SpotOptim( - fun=lambda X: np.sum(X**2, axis=1), - bounds=[(-5, 5), (-5, 5)], - acquisition="ei", # Expected Improvement - n_initial=5, - max_iter=10, - seed=777, - verbose=False, - ) - - # Initialize the optimizer by manually setting data and fitting surrogate - np.random.seed(777) - opt.X_ = np.random.rand(10, 2) * 10 - 5 # Scale to bounds [-5, 5] - opt.y_ = np.sum(opt.X_**2, axis=1) - opt.fit_surrogate(opt.X_, opt.y_) - - pickled_optimizer = dill.dumps(opt) - x_new = remote_search_task(pickled_optimizer) - - assert isinstance(x_new, np.ndarray) - assert x_new.shape == (1, 2) - - -def test_remote_search_task_different_seeds(): - """Test that different seeds produce different infill points.""" - results = [] - - for seed in [0, 1, 2]: - opt = SpotOptim( - fun=lambda X: np.sum(X**2, axis=1), - bounds=[(-5, 5), (-5, 5)], - n_initial=5, - max_iter=10, - seed=seed, - verbose=False, - ) - - # Initialize the optimizer by manually setting data and fitting surrogate - np.random.seed(seed) - opt.X_ = np.random.rand(10, 2) * 10 - 5 # Scale to bounds [-5, 5] - opt.y_ = np.sum(opt.X_**2, axis=1) - opt.fit_surrogate(opt.X_, opt.y_) - - pickled_optimizer = dill.dumps(opt) - x_new = remote_search_task(pickled_optimizer) - results.append(x_new) - - # With different seeds, we should get different points - # (though theoretically they could be the same by chance) - assert not np.allclose(results[0], results[1]) or not np.allclose( - results[1], results[2] - ) - - -def test_remote_search_task_error_handling(): - """Test error handling when optimizer fails.""" - - # Create an optimizer with an invalid configuration that will fail - class FailingOptimizer: - def suggest_next_infill_point(self): - raise ValueError("Intentional failure for testing") - - failing_opt = FailingOptimizer() - pickled_optimizer = dill.dumps(failing_opt) - - result = remote_search_task(pickled_optimizer) - - # Should return the Exception, not raise it - assert isinstance(result, Exception) - assert isinstance(result, ValueError) - assert "Intentional failure" in str(result) - - -def test_remote_search_task_preserves_optimizer_state(): - """Test that the optimizer state is correctly preserved through pickling.""" - opt = SpotOptim( - fun=lambda X: np.sum(X**2, axis=1), - bounds=[(-5, 5), (-5, 5)], - n_initial=5, - max_iter=10, - seed=42, - verbose=False, - ) - - # Initialize the optimizer by manually setting data and fitting surrogate - np.random.seed(42) - opt.X_ = np.random.rand(10, 2) * 10 - 5 # Scale to bounds [-5, 5] - opt.y_ = np.sum(opt.X_**2, axis=1) - opt.fit_surrogate(opt.X_, opt.y_) - - # Store original seed - original_seed = opt.seed - - pickled_optimizer = dill.dumps(opt) - x_new = remote_search_task(pickled_optimizer) - - # Unpickle to verify state was preserved - opt_unpickled = dill.loads(pickled_optimizer) - assert opt_unpickled.seed == original_seed - assert isinstance(x_new, np.ndarray) diff --git a/tests/test_restart_inject_parallel.py b/tests/test_restart_inject_parallel.py deleted file mode 100644 index 7e314c9d..00000000 --- a/tests/test_restart_inject_parallel.py +++ /dev/null @@ -1,149 +0,0 @@ -# SPDX-FileCopyrightText: 2026 bartzbeielstein -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -"""Tests for restart injection in the parallel (steady-state) optimisation path. - -Verify that when y0_known and self.x0 are set, the matching point in the -initial design is not re-evaluated by the worker pool, and that the stored -best solution from the previous run is correctly carried into the next run. -""" - -import time - -import numpy as np - -from spotoptim import SpotOptim -from spotoptim.function import sphere - - -class CountingObjective: - """Sphere function that records every point it is called with.""" - - def __init__(self): - self.calls = [] - - def __call__(self, X): - for x in X: - self.calls.append(x.copy()) - return np.sum(X**2, axis=1) - - def was_called_with(self, x, tol=1e-9): - return any(np.linalg.norm(c - x) < tol for c in self.calls) - - -def test_restart_inject_skips_reeval_parallel(): - """The injected best point must not be re-evaluated in the parallel path.""" - obj = CountingObjective() - - opt = SpotOptim( - fun=obj, - bounds=[(-5, 5), (-5, 5)], - n_initial=5, - max_iter=10, - seed=0, - n_jobs=2, - x0=np.array([1.0, 1.0]), - ) - - # Inject a known best value; x0=[1,1] matches the injected point. - y0_known = 2.0 # sphere([1, 1]) = 2 - obj.calls.clear() - - status, result = opt.optimize_steady_state( - timeout_start=time.time(), - X0=None, - y0_known=y0_known, - ) - - assert status == "FINISHED" - assert not obj.was_called_with( - np.array([1.0, 1.0]) - ), "Injected best point [1, 1] should not have been re-evaluated by the worker pool." - - -def test_restart_inject_result_stored_parallel(): - """The injected best value must appear in the stored y_ array.""" - opt = SpotOptim( - fun=sphere, - bounds=[(-5, 5), (-5, 5)], - n_initial=5, - max_iter=10, - seed=0, - n_jobs=2, - x0=np.array([1.0, 1.0]), - ) - - y0_known = 2.0 - opt.optimize_steady_state( - timeout_start=time.time(), - X0=None, - y0_known=y0_known, - ) - - assert ( - y0_known in opt.y_ - ), "The pre-filled y0_known value must be present in opt.y_ after the run." - - -def test_restart_inject_no_known_unchanged(): - """Without y0_known, all n_initial points must reach storage (none skipped).""" - opt = SpotOptim( - fun=sphere, - bounds=[(-5, 5), (-5, 5)], - n_initial=5, - max_iter=5, - seed=0, - n_jobs=2, - ) - - status, result = opt.optimize_steady_state( - timeout_start=time.time(), - X0=None, - y0_known=None, - ) - - assert status == "FINISHED" - # All n_initial points must be stored — no injection means nothing was skipped. - assert len(opt.y_) == opt.n_initial - assert np.all(np.isfinite(opt.y_)) - - -def test_restart_inject_parallel_eval_count(): - """With one injected point, y_ still contains n_initial entries and the - injected value is stored at exact float precision (not re-evaluated). - - Note: evaluations run in separate worker processes (ProcessPoolExecutor on - GIL builds), so in-process call counters cannot be used here. Instead we - verify the observable storage state: n_initial rows in X_/y_ and the - injected value appearing verbatim in y_. - """ - x_inject = np.array([0.5, -0.5]) - y_inject = float(np.sum(x_inject**2)) - - opt = SpotOptim( - fun=sphere, - bounds=[(-5, 5), (-5, 5)], - n_initial=6, - max_iter=6, - seed=1, - n_jobs=2, - x0=x_inject, - ) - - opt.optimize_steady_state( - timeout_start=time.time(), - X0=None, - y0_known=y_inject, - ) - - # All n_initial points must be in storage (injected + n_initial-1 via pool). - assert ( - len(opt.y_) == opt.n_initial - ), f"Expected {opt.n_initial} stored evaluations, got {len(opt.y_)}." - # The injected value must appear at exact float precision — proof it was - # stored directly and not recalculated in a worker (sphere may differ by - # floating-point rounding for transformed input coordinates). - assert ( - y_inject in opt.y_ - ), f"Injected y_known={y_inject} not found in opt.y_={opt.y_}." diff --git a/tests/test_run_sequential_loop_example.py b/tests/test_run_sequential_loop_example.py index 4cfb6b49..5dedbdda 100644 --- a/tests/test_run_sequential_loop_example.py +++ b/tests/test_run_sequential_loop_example.py @@ -16,7 +16,6 @@ def test_run_sequential_loop_example(): n_initial=5, max_iter=20, seed=0, - n_jobs=1, verbose=True, ) diff --git a/tests/test_sequential_equivalence_regression.py b/tests/test_sequential_equivalence_regression.py new file mode 100644 index 00000000..7af768a9 --- /dev/null +++ b/tests/test_sequential_equivalence_regression.py @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: 2026 bartzbeielstein +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Regression test: the sequential engine is deterministic and converges. + +Guards the single sequential optimization engine (after the parallel subsystem +was removed) against behavioural regressions, using **platform-portable** +invariants: + +1. Same-seed determinism — two runs with the same seed are bit-for-bit identical + *within a platform* (this is the property the parallelism removal had to + preserve, and it is checked exactly with ``assert_array_equal``). +2. Evaluation budget — ``nfev`` / ``nit`` / ``success`` are budget-controlled and + therefore platform-independent; checked exactly against the captured golden. +3. Convergence quality — the best value stays in the same ballpark as the golden. + +Note on byte-identity across platforms: seeded SpotOptim results are bit-identical +only on the *same* platform/BLAS. Across macOS<->Linux the iterative surrogate +trajectory amplifies floating-point rounding into different (but equally valid) +optima, so the per-coordinate ``best_x`` / history are intentionally **not** +asserted bit-exact against a fixture captured on one machine. The golden fixture +(``fixtures/sequential_golden.json``) supplies the case definitions and the +budget/quality references. +""" + +import json +import pathlib + +import numpy as np +import pytest + +from spotoptim import SpotOptim +from spotoptim.function.so import sphere, rosenbrock + +_FIXTURE_PATH = pathlib.Path(__file__).parent / "fixtures" / "sequential_golden.json" + +# Mapping from the string stored in the fixture to the actual function object. +_FUN_MAP = { + "sphere": sphere, + "rosenbrock": rosenbrock, +} + + +def _load_cases(): + """Return list of (case_id, kwargs, expected_result) tuples.""" + with _FIXTURE_PATH.open() as fh: + data = json.load(fh) + cases = [] + for case_id, entry in data.items(): + raw_kwargs = entry["kwargs"] + kwargs = dict(raw_kwargs) + kwargs["fun"] = _FUN_MAP[raw_kwargs["fun"]] + # bounds stored as list-of-lists → list of tuples + kwargs["bounds"] = [tuple(b) for b in raw_kwargs["bounds"]] + cases.append((case_id, kwargs, entry["result"])) + return cases + + +_CASES = _load_cases() + + +@pytest.mark.parametrize("case_id,kwargs,expected", _CASES, ids=[c[0] for c in _CASES]) +def test_sequential_equivalence(case_id, kwargs, expected): + """Sequential engine is deterministic, budget-correct, and converges.""" + r1 = SpotOptim(**kwargs).optimize() + r2 = SpotOptim(**kwargs).optimize() + + # 1. Same-seed determinism — bit-identical within a platform. + np.testing.assert_array_equal( + np.asarray(r1.X), np.asarray(r2.X), err_msg=f"[{case_id}] non-deterministic X" + ) + np.testing.assert_array_equal( + np.asarray(r1.y), np.asarray(r2.y), err_msg=f"[{case_id}] non-deterministic y" + ) + np.testing.assert_array_equal( + np.asarray(r1.x), np.asarray(r2.x), err_msg=f"[{case_id}] non-deterministic x" + ) + assert r1.fun == r2.fun, f"[{case_id}] non-deterministic fun" + + # 2. Evaluation budget is exact and platform-independent. + assert ( + r1.nfev == expected["nfev"] + ), f"[{case_id}] nfev {r1.nfev} != {expected['nfev']}" + assert r1.nit == expected["nit"], f"[{case_id}] nit {r1.nit} != {expected['nit']}" + assert ( + r1.success == expected["success"] + ), f"[{case_id}] success {r1.success} != {expected['success']}" + + # 3. Convergence quality stays in the golden ballpark (generous tolerance + # absorbs cross-platform floating-point trajectory divergence; a real + # regression that fails to converge would be orders of magnitude worse). + assert ( + r1.fun <= expected["fun"] * 10.0 + 1e-3 + ), f"[{case_id}] fun {r1.fun} regressed vs golden {expected['fun']}" diff --git a/tests/test_steady_state_inverse_transform.py b/tests/test_steady_state_inverse_transform.py deleted file mode 100644 index 48373ff1..00000000 --- a/tests/test_steady_state_inverse_transform.py +++ /dev/null @@ -1,84 +0,0 @@ -# SPDX-FileCopyrightText: 2026 bartzbeielstein -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -"""Regression tests for natural-scale storage in the steady-state path. - -The steady-state (``n_jobs > 1``) loop must convert evaluated points to natural -scale via ``inverse_transform_X`` before storing them in ``X_`` -- mirroring the -sequential ``update_storage`` path. Before the fix, points were stored in -transformed scale, so a ``log10`` variable was re-transformed when the surrogate -was refit, yielding ``log10`` of a negative number => ``NaN`` => a crash in the -Gaussian-process fit (``ValueError: Input X contains NaN``). -""" - -import numpy as np - -from spotoptim import SpotOptim - - -def _obj(X): - """Objective on the (log10) float column only; ignores the factor column.""" - import numpy as np - - X = np.atleast_2d(X) - return np.array([float(np.asarray(row[0], dtype=float)) for row in X]) - - -def test_steady_state_log10_does_not_crash_and_stores_natural_scale(): - bounds = [(0.001, 0.1), ["A", "B", "C"]] - var_type = ["float", "factor"] - var_trans = ["log10", None] - - opt = SpotOptim( - fun=_obj, - bounds=bounds, - var_type=var_type, - var_trans=var_trans, - n_initial=5, - max_iter=10, - seed=7, - n_jobs=2, - verbose=False, - ) - - # Must not raise "ValueError: Input X contains NaN". - opt.optimize() - - col0 = opt.X_[:, 0].astype(float) - # Natural scale: every stored value lies within the original [0.001, 0.1] - # bound, never in the log10-internal range [-3, -1]. - assert np.all(col0 >= 0.001 - 1e-9) - assert np.all(col0 <= 0.1 + 1e-9) - assert np.all(np.isfinite(col0)) - assert np.all(np.isfinite(opt.y_)) - - -def test_steady_state_matches_sequential_initial_design_scale(): - """Sequential and steady-state must store the seeded initial design in the - same (natural) scale -- the first ``n_initial`` rows coincide.""" - bounds = [(0.001, 0.1)] - var_trans = ["log10"] - - def build(n_jobs): - opt = SpotOptim( - fun=_obj, - bounds=bounds, - var_trans=var_trans, - n_initial=6, - max_iter=8, - seed=11, - n_jobs=n_jobs, - verbose=False, - ) - opt.optimize() - return np.asarray(opt.X_[:, 0], dtype=float) - - seq = build(1) - par = build(2) - # The seeded initial design (same seed) is identical across paths once both - # store natural scale. - n = min(6, len(seq), len(par)) - assert np.allclose(np.sort(seq[:n]), np.sort(par[:n]), rtol=1e-6, atol=1e-9) - # And both lie in the natural bound. - assert np.all(par >= 0.001 - 1e-9) and np.all(par <= 0.1 + 1e-9) diff --git a/tests/test_tensorboard_parallel.py b/tests/test_tensorboard_parallel.py deleted file mode 100644 index 3ba372a3..00000000 --- a/tests/test_tensorboard_parallel.py +++ /dev/null @@ -1,142 +0,0 @@ -# SPDX-FileCopyrightText: 2026 bartzbeielstein -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -import os - -import numpy as np -import pytest -from tensorboard.backend.event_processing.event_accumulator import EventAccumulator - -from spotoptim import SpotOptim - - -@pytest.fixture(autouse=True) -def _isolate_cwd(tmp_path, monkeypatch): - monkeypatch.chdir(tmp_path) - - -def dummy_func(X): - return np.sum(X**2, axis=1) - - -def _count_scalar_steps(logdir, tag="success_rate"): - """Return the number of distinct steps logged for a scalar tag. - - add_scalar events land in the run root; add_hparams events land in - per-call subdirectories, so every event file under ``logdir`` is read. - """ - steps = set() - for root, _dirs, files in os.walk(str(logdir)): - if any(f.startswith("events.out.tfevents") for f in files): - acc = EventAccumulator(root) - acc.Reload() - if tag in acc.Tags().get("scalars", []): - steps.update(e.step for e in acc.Scalars(tag)) - return len(steps) - - -def test_tensorboard_enabled_in_parallel(capsys): - """Test that TensorBoard is ENABLED when n_jobs > 1 (steady-state).""" - opt = SpotOptim( - fun=dummy_func, - bounds=[(-5, 5)], - n_initial=4, - max_iter=6, - n_jobs=2, - tensorboard_log=True, - verbose=True, - ) - - # Manually trigger initialization (usually called at start of optimize, after initial design) - # We need to simulate having some data for it to log - opt.X_ = np.array([[0.0], [1.0], [2.0], [3.0]]) - opt.y_ = np.array([0.0, 1.0, 4.0, 9.0]) - - # Initialize stats that _write_tensorboard_scalars expects - opt.min_y = 0.0 - opt.min_mean_y = 0.0 - opt.min_var_y = 0.0 - opt.min_mean_X = opt.X_[0] - opt.min_X = opt.X_[0] - opt.success_rate = 0.0 - opt._init_tensorboard() - - # Check that tb_writer is NOT None - assert opt.tb_writer is not None, "tb_writer should be enabled when n_jobs > 1" - - # Check that config was updated - assert opt.config.tensorboard_log is True, "config.tensorboard_log should stay True" - - # Check for enabled message - captured = capsys.readouterr() - assert "TensorBoard logging enabled" in captured.out - - # Ensure optimization runs without pickling error - # (SpotOptim handles tb_writer removal during dill serialization) - opt.optimize() - - -def test_parallel_logs_infill_evals(tmp_path): - """Steady-state parallel runs log scalars beyond the initial design. - - Regression test for the gap where workers carry ``tb_writer=None`` and - the parent result loop never wrote per-eval scalars, so parallel runs - logged only the initial design (one step). - """ - n_initial, max_iter = 4, 12 - path = str(tmp_path / "tb_parallel") - opt = SpotOptim( - fun=dummy_func, - bounds=[(-5, 5)], - n_initial=n_initial, - max_iter=max_iter, - n_jobs=2, - tensorboard_log=True, - tensorboard_path=path, - seed=0, - verbose=False, - ) - opt.optimize() - - assert len(opt.y_) > n_initial, "expected infill evaluations beyond n_initial" - # Before the fix, success_rate appeared at a single step (initial design - # only). With per-eval parent-side logging it advances with each batch. - assert _count_scalar_steps(path, "success_rate") > 1 - - -def test_parallel_no_tensorboard_regression(tmp_path): - """tensorboard_log=False parallel run is unaffected: no writer, no runs dir.""" - opt = SpotOptim( - fun=dummy_func, - bounds=[(-5, 5)], - n_initial=4, - max_iter=10, - n_jobs=2, - tensorboard_log=False, - seed=0, - verbose=False, - ) - res = opt.optimize() - - assert opt.tb_writer is None - assert res.success is True - assert not (tmp_path / "runs").exists() - - -def test_tensorboard_enabled_in_sequential(): - """Test that TensorBoard IS enabled when n_jobs = 1.""" - opt = SpotOptim( - fun=dummy_func, - bounds=[(-5, 5)], - n_initial=4, - max_iter=6, - n_jobs=1, - tensorboard_log=True, - verbose=False, - ) - - assert opt.tb_writer is not None, "tb_writer should NOT be None when n_jobs = 1" - # Cleanup - if opt.tb_writer: - opt.tb_writer.close() diff --git a/tests/test_termination_criteria.py b/tests/test_termination_criteria.py index 8f3bebfb..777add9b 100644 --- a/tests/test_termination_criteria.py +++ b/tests/test_termination_criteria.py @@ -182,78 +182,6 @@ def slow_sphere(x): assert elapsed >= 1.5 assert res.nfev < max_iter - def test_parallel_max_iter(self): - """Test Case 6: Parallel execution with max_iter.""" - max_iter = 12 - n_jobs = 2 - # n_initial=4. - # Run 1: 4 initial + 4 optimization (split across 2 jobs? No, SpotOptim parallel runs *independent* restart chains) - # Wait, SpotOptim parallelization is currently implemented as *independent restarts*. - # So if n_jobs=2, it launches 2 independent optimizations. - # The logic in SpotOptim is: - # while budget_remains: - # launch n_jobs tasks (each gets remaining budget?) - # Actually, let's check the implementation logic I saw earlier. - # It checked `remaining_iter` and passed it to sub-tasks. - # If I have global max_iter=12. - # It launches 2 tasks. Each might run until completion or budget? - # If they run in parallel, they consume budget. - # The test should mostly ensure it doesn't crash and returns a result, - # and roughly respects budget (maybe slightly over due to parallel batch). - - opt = SpotOptim( - fun=sphere_1d, - bounds=[(-5, 5)], - max_iter=max_iter, - n_initial=4, - n_jobs=n_jobs, - seed=42, - verbose=False, - ) - res = opt.optimize() - - # In parallel restarts, total evaluations will be sum of all runs. - # It might go slightly over max_iter if the last batch finishes. - # E.g. start with 0 evals. Launch 2 jobs. - # Each job sees "remaining=12". - # Job 1 does 12 evals. Job 2 does 12 evals. - # Total 24? That would be bad budget management. - # But let's verify what happens. - # If logic passes `remaining_iter` as `max_iter_override`, then they might both run 12. - # We'll assert it's at least max_iter. - assert res.nfev >= max_iter - # And hopefully not DOUBLE (unless that's the current behavior, which we might want to fix later, - # but for now we test 'termination works' i.e. it stops). - - def test_parallel_max_time(self): - """Test Case 7: Parallel execution with max_time.""" - - def slow_sphere(x): - time.sleep(0.05) - return np.sum(x**2, axis=1) - - max_time_min = 3.0 / 60.0 # 3 seconds - - start = time.time() - opt = SpotOptim( - fun=slow_sphere, - bounds=[(-5, 5)], - max_iter=np.inf, # Infinite budget - max_time=max_time_min, - n_initial=5, - n_jobs=2, - verbose=False, - ) - _ = opt.optimize() - elapsed = time.time() - start - - # It should stop roughly around 3 seconds - assert elapsed >= 2.5 - # It shouldn't run forever. The ceiling is generous because shared CI - # runners can stall the parallel pool teardown by several seconds; the - # assertion's intent is "the loop terminated", not a tight timing budget. - assert elapsed < 20.0 - def test_verbose_output_sequential(self, capsys): """Test Case 8: Verbose output in sequential mode.""" opt = SpotOptim( @@ -273,32 +201,3 @@ def test_verbose_output_sequential(self, capsys): or "Best:" in captured.out or "evaluations" in captured.out ) - - def test_verbose_output_parallel(self, capsys): - """Test Case 9: Verbose output in parallel mode.""" - opt = SpotOptim( - fun=sphere_1d, - bounds=[(-5, 5)], - max_iter=10, - n_initial=4, - n_jobs=2, - verbose=True, - ) - opt.optimize() - - captured = capsys.readouterr() - # In parallel steady-state, we look for "Submitted X initial points" - assert "Submitted 4 initial points" in captured.out - - # User requested guarantee that status is shown. - # We assert that we see status updates from workers. - # Note: output ordering might be interleaved. - # But "Iter" should appear at least once if verbose=True. - # If this fails, SpotOptim parallel implementation prevents worker stdout from reaching main stdout. - # In that case, we might need to adjust joblib backend or verbose settings. - # We check both stdout and stderr (joblib often prints to stderr). - combined_output = captured.out + captured.err - assert any( - x in combined_output - for x in ["Iter", "Best", "evaluations", "Parallel", "Done"] - ) diff --git a/tests/test_thread_pool_search.py b/tests/test_thread_pool_search.py deleted file mode 100644 index 78b19230..00000000 --- a/tests/test_thread_pool_search.py +++ /dev/null @@ -1,283 +0,0 @@ -# SPDX-FileCopyrightText: 2026 bartzbeielstein -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -"""Tests for Release 0.8.0 — Improvement C: ThreadPoolExecutor for search tasks. - -Verifies that: -- optimize_steady_state uses ThreadPoolExecutor for search and ProcessPoolExecutor - for evaluation (hybrid executor design). -- The surrogate lock prevents concurrent refit/search races. -- End-to-end results are correct for n_jobs > 1 with various callable types. -- Backward-compatible: remote_search_task still works (external API unchanged). -""" - -import threading -import numpy as np -from concurrent.futures import ThreadPoolExecutor -from spotoptim import SpotOptim - - -def sphere(X): - X = np.atleast_2d(X) - return np.sum(X**2, axis=1) - - -BOUNDS = [(-5, 5), (-5, 5)] - - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - - -class _LockSpy: - """Wraps a threading.Lock and records acquire/release calls.""" - - def __init__(self): - self._lock = threading.Lock() - self.acquire_count = 0 - - def acquire(self, *args, **kwargs): - self.acquire_count += 1 - return self._lock.acquire(*args, **kwargs) - - def release(self): - return self._lock.release() - - def __enter__(self): - self.acquire() - return self - - def __exit__(self, *args): - self.release() - - -# --------------------------------------------------------------------------- -# Unit: surrogate lock correctness -# --------------------------------------------------------------------------- - - -class TestSurrogateLock: - """Verify the threading.Lock serialises search and refit correctly.""" - - def test_lock_prevents_concurrent_access(self): - """Two threads cannot hold the lock simultaneously.""" - lock = threading.Lock() - results = [] - - def worker(): - acquired = lock.acquire(blocking=False) - results.append(acquired) - if acquired: - threading.Event().wait(0.05) # hold briefly - lock.release() - - t1 = threading.Thread(target=worker) - t2 = threading.Thread(target=worker) - t1.start() - t2.start() - t1.join() - t2.join() - # At most one thread could acquire the lock - assert results.count(True) == 1 - - def test_thread_pool_executor_available(self): - """ThreadPoolExecutor is importable and functional.""" - results = [] - with ThreadPoolExecutor(max_workers=2) as pool: - futs = [pool.submit(lambda x=i: x * 2, i) for i in range(4)] - results = [f.result() for f in futs] - assert sorted(results) == [0, 2, 4, 6] - - -# --------------------------------------------------------------------------- -# Integration: end-to-end optimization with n_jobs > 1 -# --------------------------------------------------------------------------- - - -class TestHybridExecutorEndToEnd: - """Smoke tests: hybrid executor produces correct optimization results.""" - - def test_n_jobs_2_sphere(self): - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=6, - max_iter=12, - n_jobs=2, - seed=42, - ) - result = opt.optimize() - assert result.success - assert result.nfev >= 6 - assert result.fun < 50.0 # sphere minimum is 0 - - def test_n_jobs_3_sphere(self): - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=6, - max_iter=12, - n_jobs=3, - seed=7, - ) - result = opt.optimize() - assert result.success - assert result.fun < 50.0 - - def test_lambda_objective_parallel(self): - """Lambda functions must survive dill serialization for eval tasks.""" - opt = SpotOptim( - fun=lambda X: np.sum(X**2, axis=1), - bounds=BOUNDS, - n_initial=5, - max_iter=10, - n_jobs=2, - seed=0, - ) - result = opt.optimize() - assert result.success - - def test_closure_objective_parallel(self): - """Closures must survive dill serialization for eval tasks.""" - scale = 2.0 - - def scaled_sphere(X): - return scale * np.sum(X**2, axis=1) - - opt = SpotOptim( - fun=scaled_sphere, - bounds=BOUNDS, - n_initial=5, - max_iter=10, - n_jobs=2, - seed=1, - ) - result = opt.optimize() - assert result.success - - def test_result_structure_matches_sequential(self): - """n_jobs=2 and n_jobs=1 produce results with identical shapes.""" - opt_seq = SpotOptim( - fun=sphere, bounds=BOUNDS, n_initial=5, max_iter=10, n_jobs=1, seed=0 - ) - opt_par = SpotOptim( - fun=sphere, bounds=BOUNDS, n_initial=5, max_iter=10, n_jobs=2, seed=0 - ) - r_seq = opt_seq.optimize() - r_par = opt_par.optimize() - - assert r_seq.success - assert r_par.success - assert r_par.X.shape[1] == r_seq.X.shape[1] # same number of dimensions - assert r_par.fun < 100.0 - - def test_minus_one_n_jobs_parallel(self): - """n_jobs=-1 (all cores) triggers the hybrid executor path.""" - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=5, - max_iter=10, - n_jobs=-1, - seed=3, - ) - assert opt.n_jobs >= 1 - result = opt.optimize() - assert result.success - - def test_4d_sphere_parallel(self): - """Higher-dimensional problem with n_jobs=2.""" - bounds_4d = [(-3, 3)] * 4 - opt = SpotOptim( - fun=sphere, - bounds=bounds_4d, - n_initial=8, - max_iter=16, - n_jobs=2, - seed=99, - ) - result = opt.optimize() - assert result.success - assert result.X.shape[1] == 4 - - -# --------------------------------------------------------------------------- -# Backward compatibility: remote_search_task still works -# --------------------------------------------------------------------------- - - -class TestRemoteSearchTaskBackwardCompat: - """remote_search_task (dill-based) must remain importable and functional.""" - - def test_import_remote_search_task(self): - from spotoptim.utils.parallel import remote_search_task # noqa: F401 - - assert callable(remote_search_task) - - def test_remote_search_task_returns_array(self): - import dill - from spotoptim.utils.parallel import remote_search_task - - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=5, - max_iter=10, - seed=0, - ) - np.random.seed(0) - opt.X_ = np.random.uniform(-5, 5, (8, 2)) - opt.y_ = sphere(opt.X_) - opt.fit_surrogate(opt.X_, opt.y_) - - pickled = dill.dumps(opt) - result = remote_search_task(pickled) - assert isinstance(result, np.ndarray) - assert result.ndim == 2 - assert result.shape[1] == 2 - - -# --------------------------------------------------------------------------- -# Thread-safety: _surrogate_lock guards concurrent surrogate access -# --------------------------------------------------------------------------- - - -class TestThreadSafety: - """Verify no data race when search threads and main-thread refit coexist.""" - - def test_concurrent_suggest_calls_do_not_crash(self): - """Multiple threads calling suggest_next_infill_point() under a lock - must not raise or corrupt state.""" - opt = SpotOptim( - fun=sphere, - bounds=BOUNDS, - n_initial=8, - max_iter=20, - n_jobs=1, - seed=42, - ) - # Prime the surrogate with some data - X_init = np.random.default_rng(0).uniform(-5, 5, (8, 2)) - opt.X_ = X_init - opt.y_ = sphere(X_init) - opt.fit_surrogate(opt.X_, opt.y_) - - lock = threading.Lock() - errors = [] - - def suggest_with_lock(): - try: - with lock: - opt.suggest_next_infill_point() - except Exception as e: - errors.append(e) - - threads = [threading.Thread(target=suggest_with_lock) for _ in range(4)] - for t in threads: - t.start() - for t in threads: - t.join() - - assert errors == [], f"Thread errors: {errors}" diff --git a/tests/test_update_storage_steady_example.py b/tests/test_update_storage_steady_example.py deleted file mode 100644 index 58611ec6..00000000 --- a/tests/test_update_storage_steady_example.py +++ /dev/null @@ -1,23 +0,0 @@ -# SPDX-FileCopyrightText: 2026 bartzbeielstein -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -import numpy as np - -from spotoptim import SpotOptim - - -def test_update_storage_steady_example(): - opt = SpotOptim( - fun=lambda X: np.sum(X**2, axis=1), - bounds=[(-5, 5), (-5, 5)], - n_jobs=2, - ) - - opt._update_storage_steady(np.array([1.0, 2.0]), 5.0) - - assert opt.X_.shape == (1, 2) - assert np.allclose(opt.X_, np.array([[1.0, 2.0]])) - assert np.allclose(opt.y_, np.array([5.0])) - assert np.allclose(opt.best_x_, np.array([1.0, 2.0])) - assert opt.best_y_ == 5.0 diff --git a/uv.lock b/uv.lock index f86b93c1..a94dc729 100644 --- a/uv.lock +++ b/uv.lock @@ -697,20 +697,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, ] -[[package]] -name = "flake8" -version = "7.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mccabe" }, - { name = "pycodestyle" }, - { name = "pyflakes" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, -] - [[package]] name = "fonttools" version = "4.63.0" @@ -1526,15 +1512,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, ] -[[package]] -name = "mccabe" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -1746,15 +1723,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, ] -[[package]] -name = "nvidia-nccl-cu12" -version = "2.30.4" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/2b/1757b6b74ee241de5efee3f35487dcb33e09c07605254809c6ce36aeb783/nvidia_nccl_cu12-2.30.4-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:606fa9aa9215c00367d060188eb1a5bbd28396aff5e11b9200d99d1a6ab79a71", size = 300091935, upload-time = "2026-04-23T03:22:58.024Z" }, - { url = "https://files.pythonhosted.org/packages/6b/c3/0e45ff4dce8401f6ea7c25d80d75738813a47f5ae2691e2478f2fd1e5e93/nvidia_nccl_cu12-2.30.4-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:040974b261edec4b8b793e59e92ab7176fe4ab4bc61b800f9f3bfaeec2d436f3", size = 300164158, upload-time = "2026-04-23T03:23:19.589Z" }, -] - [[package]] name = "packaging" version = "26.2" @@ -2050,15 +2018,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] -[[package]] -name = "pycodestyle" -version = "2.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, -] - [[package]] name = "pycparser" version = "3.0" @@ -2139,15 +2098,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, ] -[[package]] -name = "pyflakes" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -2929,40 +2879,44 @@ name = "spotoptim" version = "0.12.9" source = { editable = "." } dependencies = [ - { name = "black" }, { name = "dill" }, - { name = "flake8" }, - { name = "importlib-metadata" }, - { name = "jupyter" }, - { name = "matplotlib" }, { name = "numpy" }, { name = "pandas" }, - { name = "requests" }, { name = "scikit-learn" }, { name = "scipy" }, + { name = "tabulate" }, +] + +[package.optional-dependencies] +all = [ + { name = "matplotlib" }, + { name = "requests" }, { name = "seaborn" }, - { name = "spotdesirability" }, { name = "statsmodels" }, - { name = "tabulate" }, { name = "tensorboard" }, { name = "torch", version = "2.12.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, { name = "torch", version = "2.12.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" }, - { name = "ty" }, - { name = "xgboost" }, ] - -[package.optional-dependencies] dev = [ { name = "bandit" }, { name = "black" }, { name = "isort" }, + { name = "matplotlib" }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-timeout" }, { name = "pytest-xdist" }, + { name = "requests" }, { name = "ruff" }, { name = "safety" }, + { name = "seaborn" }, + { name = "spotdesirability" }, + { name = "statsmodels" }, + { name = "tensorboard" }, + { name = "torch", version = "2.12.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, + { name = "torch", version = "2.12.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" }, + { name = "ty" }, ] docs = [ { name = "colorama" }, @@ -2971,6 +2925,21 @@ docs = [ { name = "jupyter" }, { name = "quartodoc" }, ] +remote = [ + { name = "requests" }, +] +stats = [ + { name = "statsmodels" }, +] +torch = [ + { name = "tensorboard" }, + { name = "torch", version = "2.12.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, + { name = "torch", version = "2.12.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" }, +] +viz = [ + { name = "matplotlib" }, + { name = "seaborn" }, +] [package.dev-dependencies] dev = [ @@ -2984,23 +2953,20 @@ dev = [ { name = "pytest-xdist" }, { name = "ruff" }, { name = "safety" }, + { name = "ty" }, ] [package.metadata] requires-dist = [ { name = "bandit", marker = "extra == 'dev'", specifier = ">=1.8.0" }, - { name = "black", specifier = ">=26.1.0" }, { name = "black", marker = "extra == 'dev'", specifier = ">=24.1.0" }, { name = "colorama", marker = "extra == 'docs'", specifier = ">=0.4.6" }, { name = "dill", specifier = ">=0.4.1" }, - { name = "flake8", specifier = ">=7.3.0" }, { name = "griffe", marker = "extra == 'docs'", specifier = ">=1.7.3" }, - { name = "importlib-metadata", specifier = ">=8.7.1" }, { name = "importlib-metadata", marker = "extra == 'docs'" }, { name = "isort", marker = "extra == 'dev'", specifier = ">=5.13.0" }, - { name = "jupyter", specifier = ">=1.1.1" }, { name = "jupyter", marker = "extra == 'docs'", specifier = ">=1.1.1" }, - { name = "matplotlib", specifier = ">=3.10.7" }, + { name = "matplotlib", marker = "extra == 'viz'", specifier = ">=3.10.7" }, { name = "numpy", specifier = ">=1.24.3" }, { name = "pandas", specifier = ">=2.1.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.7.0" }, @@ -3009,21 +2975,22 @@ requires-dist = [ { name = "pytest-timeout", marker = "extra == 'dev'", specifier = ">=2.3.0" }, { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.5.0" }, { name = "quartodoc", marker = "extra == 'docs'", specifier = ">=0.11.1" }, - { name = "requests", specifier = ">=2.32.3" }, + { name = "requests", marker = "extra == 'remote'", specifier = ">=2.32.3" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.3.0" }, { name = "safety", marker = "extra == 'dev'", specifier = ">=3.0.0" }, { name = "scikit-learn", specifier = ">=1.5.0" }, { name = "scipy", specifier = ">=1.10.1" }, - { name = "seaborn", specifier = ">=0.13.2" }, - { name = "spotdesirability", specifier = ">=0.0.1" }, - { name = "statsmodels", specifier = ">=0.14.6" }, + { name = "seaborn", marker = "extra == 'viz'", specifier = ">=0.13.2" }, + { name = "spotdesirability", marker = "extra == 'dev'", specifier = ">=0.0.1" }, + { name = "spotoptim", extras = ["all"], marker = "extra == 'dev'" }, + { name = "spotoptim", extras = ["torch", "viz", "stats", "remote"], marker = "extra == 'all'" }, + { name = "statsmodels", marker = "extra == 'stats'", specifier = ">=0.14.6" }, { name = "tabulate", specifier = ">=0.9.0" }, - { name = "tensorboard", specifier = ">=2.20.0" }, - { name = "torch", specifier = ">=2.9.1", index = "https://download.pytorch.org/whl/cpu" }, - { name = "ty", specifier = ">=0.0.29" }, - { name = "xgboost", specifier = ">=3.1.1" }, + { name = "tensorboard", marker = "extra == 'torch'", specifier = ">=2.20.0" }, + { name = "torch", marker = "extra == 'torch'", specifier = ">=2.9.1", index = "https://download.pytorch.org/whl/cpu" }, + { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.29" }, ] -provides-extras = ["dev", "docs"] +provides-extras = ["torch", "viz", "stats", "remote", "all", "dev", "docs"] [package.metadata.requires-dev] dev = [ @@ -3037,6 +3004,7 @@ dev = [ { name = "pytest-xdist", specifier = ">=3.5.0" }, { name = "ruff", specifier = ">=0.3.0" }, { name = "safety", specifier = ">=3.0.0" }, + { name = "ty", specifier = ">=0.0.29" }, ] [[package]] @@ -3486,24 +3454,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" }, ] -[[package]] -name = "xgboost" -version = "3.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "nvidia-nccl-cu12", marker = "sys_platform == 'linux'" }, - { name = "scipy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/91/bb/1eb0242409d22db725d7a88088e6cfd6556829fb0736f9ff69aa9f1e9455/xgboost-3.2.0.tar.gz", hash = "sha256:99b0e9a2a64896cdaf509c5e46372d336c692406646d20f2af505003c0c5d70d", size = 1263936, upload-time = "2026-02-10T11:03:05.542Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/49/6e4cdd877c24adf56cb3586bc96d93d4dcd780b5ea1efb32e1ee0de08bae/xgboost-3.2.0-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:2f661966d3e322536d9c448090a870fcba1e32ee5760c10b7c46bac7a342079a", size = 2507014, upload-time = "2026-02-10T10:50:57.44Z" }, - { url = "https://files.pythonhosted.org/packages/93/f1/c09ef1add609453aa3ba5bafcd0d1c1a805c1263c0b60138ec968f8ec296/xgboost-3.2.0-py3-none-macosx_12_0_arm64.whl", hash = "sha256:eabbd40d474b8dbf6cb3536325f9150b9e6f0db32d18de9914fb3227d0bef5b7", size = 2328527, upload-time = "2026-02-10T10:51:17.502Z" }, - { url = "https://files.pythonhosted.org/packages/96/9f/d9914a7b8df842832850b1a18e5f47aaa071c217cdd1da2ae9deb291018b/xgboost-3.2.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:852eabc6d3b3702a59bf78dbfdcd1cb9c4d3a3b6e5ed1f8781d8b9512354fdd2", size = 131100954, upload-time = "2026-02-10T11:02:42.704Z" }, - { url = "https://files.pythonhosted.org/packages/79/98/679de17c2caa4fd3b0b4386ecf7377301702cb0afb22930a07c142fcb1d8/xgboost-3.2.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:99b4a6bbcb47212fec5cf5fbe12347215f073c08967431b0122cfbd1ee70312c", size = 131748579, upload-time = "2026-02-10T10:54:40.424Z" }, - { url = "https://files.pythonhosted.org/packages/1f/3d/1661dd114a914a67e3f7ab66fa1382e7599c2a8c340f314ad30a3e2b4d08/xgboost-3.2.0-py3-none-win_amd64.whl", hash = "sha256:0d169736fd836fc13646c7ab787167b3a8110351c2c6bc770c755ee1618f0442", size = 101681668, upload-time = "2026-02-10T10:59:31.202Z" }, -] - [[package]] name = "zipp" version = "4.1.0"