diff --git a/.claude/docs/adaptive_pool_prototype.py b/.claude/docs/adaptive_pool_prototype.py new file mode 100644 index 0000000..dc8ef14 --- /dev/null +++ b/.claude/docs/adaptive_pool_prototype.py @@ -0,0 +1,225 @@ +"""Prototype: an intelligent model-selection pool that shrinks over time. + +Idea (see model-selection-loop.md, hypothesis H8): instead of cross-validating the +whole candidate pool every iteration, maintain an *active* set that shrinks as +evidence accumulates, while protecting generalization with a per-family diversity +floor and periodically re-admitting pruned models to track the (non-stationary) +landscape as the archive grows. + +This simulates a run (growing archive) and compares three strategies on +**cost** (cumulative model fits — platform-independent) and **generalization** +(held-out RMSE of the selected model): + + - full : cross-validate all candidates every iteration (current behaviour) + - family : a fixed redundancy-free pool (one model per kernel family) + - racing : the adaptive shrink-and-readmit pool proposed here + +Run: pyclawd python .claude/docs/adaptive_pool_prototype.py +""" + +import time +from collections import defaultdict, deque +from copy import deepcopy + +import numpy as np +from pymoo.problems import get_problem +from pymoo.util.normalization import NoNormalization + +from ezmodel.models.kriging import Kriging +from ezmodel.models.rbf import RBF + + +# --- candidate pool (name -> fresh-model factory) -------------------------------- + + +def make_pool(): + pool = {} + for kernel in ["linear", "cubic", "gaussian", "mq"]: + for tail in ["constant", "linear", "quadratic"]: + for norm in [False, True]: + name = f"rbf-{kernel}-{tail}-{'norm' if norm else 'raw'}" + pool[name] = ("rbf", dict(kernel=kernel, tail=tail, normalized=norm)) + for regr in ["constant", "linear", "quadratic"]: + pool[f"kriging-{regr}"] = ("kriging", dict(regr=regr)) + return pool + + +def family_of(name): + """Kernel family used for the diversity floor.""" + return name.split("-")[1] if name.startswith("rbf") else "kriging" + + +def build(spec): + kind, kw = spec + if kind == "rbf": + return RBF(norm_X=NoNormalization(), **kw) + return Kriging(**kw) + + +# --- deterministic CV scoring (mae; lower is better) ----------------------------- + + +def strided_folds(n, k): + idx = np.arange(n) + return [(idx[idx % k != f], idx[idx % k == f]) for f in range(k)] + + +def cv_mae(spec, X, y, folds): + errs = [] + for trn, tst in folds: + try: + m = build(spec) + m.fit(X[trn], y[trn]) + yh = np.asarray(m.predict(X[tst])).ravel() + errs.append(np.mean(np.abs(yh - y[tst]))) + except Exception: + return np.inf + return float(np.mean(errs)) + + +def fit_full_and_test(spec, X, y, Xte, yte): + m = build(spec) + m.fit(X, y) + yh = np.asarray(m.predict(Xte)).ravel() + return float(np.sqrt(np.mean((yh - yte) ** 2))) + + +# --- the adaptive racing pool ---------------------------------------------------- + + +class RacingPool: + """Active-set model selection that shrinks over time, with a diversity floor + and periodic re-admission. + + Parameters + ---------- + warmup : iterations before any pruning (collect evidence first) + window : rolling-window length for each model's mean CV score + keep_ratio : fraction of the active set kept at each prune step (halving-ish) + floor : never prune below this many models, and always keep >=1 per family + readmit_every : every N iterations, re-admit a round-robin batch of pruned models + readmit_batch : how many pruned models to re-admit each time + """ + + def __init__(self, pool, warmup=3, window=4, keep_ratio=0.6, floor=8, readmit_every=5, readmit_batch=4, rng=None): + self.pool = pool + self.active = list(pool.keys()) + self.pruned = [] + self.hist = defaultdict(lambda: deque(maxlen=window)) + self.warmup = warmup + self.keep_ratio = keep_ratio + self.floor = floor + self.readmit_every = readmit_every + self.readmit_batch = readmit_batch + self.rng = rng if rng is not None else np.random.default_rng(0) + self.t = 0 + + def _rolling(self, name): + h = self.hist[name] + return np.mean(h) if h else np.inf + + def _prune(self): + # rank active models by rolling mean CV error (lower is better) + ranked = sorted(self.active, key=self._rolling) + n_keep = max(self.floor, int(np.ceil(len(ranked) * self.keep_ratio))) + keep = ranked[:n_keep] + # diversity floor: ensure >=1 survivor per family represented in the pool + kept_families = {family_of(m) for m in keep} + for m in ranked[n_keep:]: + fam = family_of(m) + if fam not in kept_families: + keep.append(m) + kept_families.add(fam) + newly_pruned = [m for m in self.active if m not in keep] + self.pruned = newly_pruned + self.pruned # round-robin order for re-admission + self.active = keep + + def _readmit(self): + batch, self.pruned = self.pruned[: self.readmit_batch], self.pruned[self.readmit_batch :] + self.active = self.active + batch + + def select(self, X, y, folds): + """Score the active set, update history, return (best_name, n_fits).""" + self.t += 1 + if self.readmit_every and self.t % self.readmit_every == 0 and self.pruned: + self._readmit() + + n_fits = 0 + for name in self.active: + self.hist[name].append(cv_mae(self.pool[name], X, y, folds)) + n_fits += len(folds) + + best = min(self.active, key=lambda m: self.hist[m][-1]) + + if self.t > self.warmup and len(self.active) > self.floor: + self._prune() + return best, n_fits + + +# --- simulation ------------------------------------------------------------------ + + +def simulate(problem_name="ackley", n_var=5, n0=20, step=6, iters=20, k=5, seed=0): + prob = get_problem(problem_name, n_var=n_var) + xl, xu = prob.bounds() + rng = np.random.RandomState(seed) + Xte = rng.rand(500, n_var) * (xu - xl) + xl + yte = prob.evaluate(Xte).ravel() + + pool = make_pool() + family = {name: (kind == "rbf") for name, (kind, _) in pool.items()} # noqa: F841 + + racer = RacingPool(pool, rng=np.random.default_rng(seed)) + fixed_family = [n for n in pool if family_of(n) in {"cubic", "gaussian", "mq", "linear"} and n.endswith("norm")][:8] + + rows = [] + for t in range(iters): + n = n0 + t * step + X = rng.rand(n, n_var) * (xu - xl) + xl + y = prob.evaluate(X).ravel() + folds = strided_folds(n, k) + + # full pool every iteration + t0 = time.perf_counter() + full_scores = {name: cv_mae(spec, X, y, folds) for name, spec in pool.items()} + full_best = min(full_scores, key=full_scores.get) + full_t = time.perf_counter() - t0 + full_fits = len(pool) * k + full_rmse = fit_full_and_test(pool[full_best], X, y, Xte, yte) + + # fixed family pool + fam_scores = {name: cv_mae(pool[name], X, y, folds) for name in fixed_family} + fam_best = min(fam_scores, key=fam_scores.get) + fam_fits = len(fixed_family) * k + fam_rmse = fit_full_and_test(pool[fam_best], X, y, Xte, yte) + + # racing pool + race_best, race_fits = racer.select(X, y, folds) + race_rmse = fit_full_and_test(pool[race_best], X, y, Xte, yte) + + rows.append((n, full_fits, full_rmse, fam_fits, fam_rmse, race_fits, len(racer.active), race_rmse)) + + return rows + + +if __name__ == "__main__": + for prob in ["ackley", "rastrigin"]: + print("=" * 96) + print(f"PROBLEM: {prob} (fits = #model-fits that iteration; rmse = held-out generalization)") + print("=" * 96) + rows = simulate(prob) + hdr = f"{'n':>4} | {'full_fits':>9} {'full_rmse':>9} | {'fam_fits':>8} {'fam_rmse':>8} | {'race_fits':>9} {'race_act':>8} {'race_rmse':>9}" + print(hdr) + print("-" * len(hdr)) + for (n, ff, fr, af, ar, rf, ra, rr) in rows: + print(f"{n:>4} | {ff:>9} {fr:>9.3f} | {af:>8} {ar:>8.3f} | {rf:>9} {ra:>8} {rr:>9.3f}") + tot_full = sum(r[1] for r in rows) + tot_fam = sum(r[3] for r in rows) + tot_race = sum(r[5] for r in rows) + mean_full = np.mean([r[2] for r in rows]) + mean_fam = np.mean([r[4] for r in rows]) + mean_race = np.mean([r[7] for r in rows]) + print("-" * len(hdr)) + print(f"TOTAL fits full={tot_full} family={tot_fam} racing={tot_race} " + f"(racing/full = {tot_race / tot_full:.0%})") + print(f"MEAN rmse full={mean_full:.3f} family={mean_fam:.3f} racing={mean_race:.3f}") diff --git a/.claude/docs/loo_vs_kfold.py b/.claude/docs/loo_vs_kfold.py new file mode 100644 index 0000000..62c2e34 --- /dev/null +++ b/.claude/docs/loo_vs_kfold.py @@ -0,0 +1,120 @@ +"""Empirically check: is LOO-CV a worse model selector than k-fold CV? + +For each candidate surrogate we compute three numbers on the same training set: + - cv5 : 5-fold CV mean-absolute-error (pysamoo's current criterion) + - loo : leave-one-out CV mae (== the closed-form GP LOO error, just computed + the slow way; for GP the closed form gives identical values) + - test : the TRUE mae on a large held-out set (ground-truth generalization) + +A good selection criterion (a) ranks models like `test` does (high rank +correlation) and (b) the model it *picks* has low true `test` error. We compare +cv5 vs loo on both, across seeds and problems. + +Run: pyclawd python .claude/docs/loo_vs_kfold.py +""" + +from copy import deepcopy + +import numpy as np +from pymoo.problems import get_problem +from pymoo.util.normalization import NoNormalization + +from ezmodel.models.kriging import Kriging +from ezmodel.models.rbf import RBF + + +def make_pool(): + pool = {} + for kernel in ["cubic", "gaussian", "mq"]: + for norm in [False, True]: + pool[f"rbf-{kernel}-{'n' if norm else 'r'}"] = ("rbf", dict(kernel=kernel, normalized=norm)) + for regr in ["constant", "linear", "quadratic"]: + pool[f"kriging-{regr}"] = ("kriging", dict(regr=regr)) + return pool + + +def build(spec): + kind, kw = spec + return RBF(norm_X=NoNormalization(), **kw) if kind == "rbf" else Kriging(**kw) + + +def cv_mae(spec, X, y, folds): + errs = [] + for trn, tst in folds: + try: + m = build(spec) + m.fit(X[trn], y[trn]) + errs.append(np.mean(np.abs(np.asarray(m.predict(X[tst])).ravel() - y[tst]))) + except Exception: + return np.inf + return float(np.mean(errs)) + + +def folds_kfold(n, k, rng): + order = rng.permutation(n) + pos = np.arange(n) + return [(order[pos % k != f], order[pos % k == f]) for f in range(k)] + + +def folds_loo(n): + idx = np.arange(n) + return [(np.delete(idx, i), np.array([i])) for i in range(n)] + + +def spearman(a, b): + """Rank correlation (no scipy).""" + a, b = np.asarray(a, float), np.asarray(b, float) + ok = np.isfinite(a) & np.isfinite(b) + a, b = a[ok], b[ok] + if len(a) < 3: + return np.nan + ra, rb = np.argsort(np.argsort(a)), np.argsort(np.argsort(b)) + return float(np.corrcoef(ra, rb)[0, 1]) + + +def test_mae(spec, X, y, Xte, yte): + try: + m = build(spec) + m.fit(X, y) + return float(np.mean(np.abs(np.asarray(m.predict(Xte)).ravel() - yte))) + except Exception: + return np.inf + + +def run(problem_name, n_var=5, n=30, seeds=(0, 1, 2, 3, 4)): + prob = get_problem(problem_name, n_var=n_var) + xl, xu = prob.bounds() + pool = make_pool() + + rho_cv5, rho_loo, pick_gap_cv5, pick_gap_loo, agree = [], [], [], [], [] + for seed in seeds: + rng = np.random.RandomState(seed) + X = rng.rand(n, n_var) * (xu - xl) + xl + y = prob.evaluate(X).ravel() + Xte = rng.rand(800, n_var) * (xu - xl) + xl + yte = prob.evaluate(Xte).ravel() + + names = list(pool) + cv5 = [cv_mae(pool[m], X, y, folds_kfold(n, 5, np.random.RandomState(seed))) for m in names] + loo = [cv_mae(pool[m], X, y, folds_loo(n)) for m in names] + tst = [test_mae(pool[m], X, y, Xte, yte) for m in names] + + rho_cv5.append(spearman(cv5, tst)) + rho_loo.append(spearman(loo, tst)) + + best_test = min(tst) + pick_cv5 = names[int(np.argmin(cv5))] + pick_loo = names[int(np.argmin(loo))] + pick_gap_cv5.append(tst[names.index(pick_cv5)] - best_test) + pick_gap_loo.append(tst[names.index(pick_loo)] - best_test) + agree.append(pick_cv5 == pick_loo) + + print(f"\n{problem_name} (n_var={n_var}, n={n}, {len(seeds)} seeds, pool={len(pool)})") + print(f" rank-corr with TRUE test error : 5-fold={np.nanmean(rho_cv5):+.3f} LOO={np.nanmean(rho_loo):+.3f} (higher=better selector)") + print(f" test-error gap of PICKED model : 5-fold={np.mean(pick_gap_cv5):.4f} LOO={np.mean(pick_gap_loo):.4f} (lower=better pick)") + print(f" 5-fold and LOO pick same model : {np.mean(agree):.0%} of seeds") + + +if __name__ == "__main__": + for p in ["ackley", "rastrigin", "sphere"]: + run(p) diff --git a/.claude/docs/model-selection-loop.md b/.claude/docs/model-selection-loop.md new file mode 100644 index 0000000..5ea9447 --- /dev/null +++ b/.claude/docs/model-selection-loop.md @@ -0,0 +1,157 @@ +# Research loop: cheap, reproducible surrogate model selection + +An iterative, self-contained loop to investigate and improve pysamoo's +model-selection step. Run it yourself or drive it with `/loop`. Background and +measured evidence: [model-selection-research.md](model-selection-research.md). +Harness: [model_selection_bench.py](model_selection_bench.py). + +--- + +## Research question + +> **How can we cut the per-iteration cost of surrogate model selection by ≥10× while +> preserving (or improving) out-of-sample generalization, and make selection +> reproducible under a fixed seed?** + +Sub-questions (each is a hypothesis in the backlog): + +- **RQ1 (pool).** How small can the candidate pool be before generalization drops? + Is "one model per kernel family" enough? +- **RQ2 (CV cost).** Can closed-form LOO-CV replace k-fold refitting without changing + which model is selected? +- **RQ3 (frequency).** How often must we *re-select*? Does lazy re-selection (honor + `nth_validate` / a degradation trigger) keep convergence quality? +- **RQ4 (scale).** Does capping the GP fit to an L-nearest subset bound cost without + hurting generalization where search is active? +- **RQ5 (ensemble).** Does a PRESS-weighted ensemble beat single-best generalization + at zero extra fit cost? +- **RQ6 (determinism).** What minimal changes make selection bit-reproducible under a + fixed seed, with no quality loss? + +--- + +## Invariants (do not break these) + +1. **Generalization first.** Every candidate change is judged on **held-out test + error** (RMSE + rank correlation τ), never on training fit or runtime alone. A + speed-up that worsens test error beyond the tolerance below is rejected. +2. **Fixed evaluation suite.** Score on the same problems/sizes every iteration: + `ackley`, `rastrigin` (multimodal) and at least one `zdt`/`dtlz` for multi-objective; + archive sizes n ∈ {40, 80, 160}; seeds {0,1,2,3,4}. Extend the suite only by + *adding*, never removing. + - **Caveat (validate on REAL archives).** The harness trains on uniform random + samples generated all at once; a real optimization archive is sequential, + clustered near promising regions, and non-stationary. Cost/determinism results + are distribution-independent and hold, but **generalization/quality claims + (family≈full, racing≈full) must be confirmed on archives produced by actual + optimization runs.** Preferred realistic metric: select/fit on iteration *t*'s + archive, test on iteration *t+1*'s **actual infills** (the real prediction + task). The decisive test is end-to-end: plug a strategy into the real algorithm + and compare final IGD/best-F + wall-time. Beware CV leakage from spatial + clustering (nearby points across folds → optimistic CV). +3. **Two baselines stay in every comparison:** `full(38)` (current behaviour, + generalization reference) and the plain pymoo algorithm with no surrogate + (does the surrogate still help?). +4. **Acceptance tolerance.** A variant is "no worse" if its mean test RMSE is within + **+5 %** of `full(38)` on every suite problem. Cost must improve by the target of + the hypothesis. +5. **Determinism is a gate, not a nicety.** A variant that is faster but + non-reproducible is not "done" until RQ6 is also satisfied for it. + +--- + +## Iteration protocol + +Each loop iteration: + +1. **Pick** the top `pending` hypothesis from the backlog (highest impact/effort). +2. **Implement** it behind a flag or as a new strategy in `model_selection_bench.py` + (research first; only touch `src/pysamoo/**` once a hypothesis is confirmed and you + intend to ship it — then add a golden/regression test). +3. **Measure** cost, generalization (RMSE + τ), and determinism via the harness on the + fixed suite. +4. **Decide** against the invariants: `confirmed` (meets cost goal within tolerance), + `rejected` (hurts generalization), or `partial` (works in some regime — record + where). +5. **Record** one row in the Results log below (append-only) and update the + hypothesis status. Note any surprise as a new hypothesis. +6. **Stop** when the Stopping criteria are met; otherwise go to 1. + +--- + +## Hypothesis backlog (prioritized) + +| # | Hypothesis | Where to change | Experiment | Success criterion | Status | +|---|---|---|---|---|---| +| H1 | A redundancy-free **family pool (~8)** generalizes like full(38) at ~10× less cost | `defaults.py` pool / bench `POOLS` | family(8) vs full(38) vs small(3) on suite | ≥8× faster, RMSE within +5% | **confirmed** (ackley/rastrigin, see log) — extend to MOO + constraints | +| H2 | **Closed-form LOO-CV** (GPML eq. 5.12) selects the same model as 5-fold at a fraction of cost | new strategy; later `target.py`/`ezmodel` | LOO-rank vs kfold-rank agreement; cost | same top-1 ≥90% of cases, ≥3× faster, deterministic | **REJECTED** — `loo_vs_kfold.py`: LOO picks a worse-generalizing model than 5-fold (Ackley test-gap 0.025 vs 0.0001) and disagrees 20–40% of seeds. 5-fold is more robust; keep it. Speed must come from H3/H8/H4, not from changing the CV scheme. | +| H3 | **Lazy re-selection** every k iters (wire up dead `nth_validate`) keeps convergence | `algorithm.py` `revalidate()` gates the `_advance` validate | full-run wall + quality vs k∈{1,5} | big wall drop, quality not worse | **SHIPPED** — ~3.8× faster at equal/better quality; `nth_validate=5` is now honored (was dead code); guarded by `tests/test_selection.py` | +| H4 | **Cap GP fit to L-nearest subset** bounds O(n³) without hurting generalization | `_doe()` / fit path | RMSE & cost vs cap L∈{80,160,∞}, large n | cost ~constant in n, RMSE within +5% | pending | +| H5 | **Reproducible runs under a fixed seed** | thread `random_state` everywhere + deterministic CV folds + deterministic tie-break | run each algorithm twice, assert identical | identical results across runs | **SHIPPED** — see below; `tests/test_reproducibility.py` guards GPSAF/PSAF/SSANSGA2 | +| H6 | **PRESS-weighted ensemble** ≥ single-best generalization, free uncertainty | `target.py` find_best/predict | ensemble vs best RMSE on suite | RMSE ≤ best, no extra fits | pending | +| H7 | **Rank by log pseudo-likelihood** (not integer kendall_tau) removes frequent ties | `target.py` indicators | tie frequency; selection stability | fewer ties, stable choice | pending | +| H8 | **Adaptive racing pool**: shrink the active set over time (prune dominated models by rolling CV error) with a per-family diversity floor + periodic re-admission | new `RacingTarget` / `target.py` | cost (fits/wall) + generalization vs full, over a simulated run | ≥40% fewer fits, RMSE within tol, still adapts | **confirmed (prototype)** — see `adaptive_pool_prototype.py`; matches full RMSE at 56% of fits | + +--- + +## Stopping criteria + +Stop when **either**: +- a combination (expected: H1 + H2 + H3, optionally H4) achieves **≥10× median cost + reduction** on the suite at large n **with** mean test RMSE within +5 % of full(38) + **and** reproducible selection; or +- the backlog is exhausted (all `confirmed`/`rejected`) — then write up the + recommended default configuration and open a PR that ships it with golden tests. + +--- + +## Results log (append-only) + +| date | iter | hypothesis | setup | cost | generalization (test RMSE) | determinism | verdict | +|---|---|---|---|---|---|---|---| +| 2026-06-27 | 0 | baseline | full(38), 5-fold, ackley/rastrigin n=40/80 | 0.7–1.4 s | ackley 0.70–0.80; rastrigin 15.7–18.8 | **no** (unseeded folds) | reference | +| 2026-06-27 | 0 | H1 first look | small(3) | 0.02 s | ackley 4.5–33; rastrigin 149–171 | n/a | **rejected** (kills generalization) | +| 2026-06-27 | 1 | H1 | family(8) | 0.07–0.12 s | ackley 0.69–0.80; rastrigin 15.7–18.5 (≈full) | n/a | **confirmed** (~10× faster, RMSE within tol) — TODO: MOO + constraints | +| 2026-06-27 | 1 | H5 | full(38), seeded folds | same | unchanged | **yes** (identical across runs) | **confirmed** | +| 2026-06-27 | 2 | H5 | **SHIPPED**: random_state threaded through GPSAF/PSAF/SSANSGA2 + deterministic CV folds (`randomize=False`) + deterministic tie-break (`models[0]`) | unchanged | unchanged | **yes** — all 3 algos bit-identical across runs | **done** — guarded by `tests/test_reproducibility.py` | +| 2026-06-27 | 3 | H8 | racing pool (warmup 3, window 4, keep 0.6, floor 8, re-admit 4 every 5) vs full, simulated 20-iter growing archive (ackley, rastrigin) | **56% of full's fits** (and the *late, expensive* O(n³) fits run on the shrunk pool) | **identical to full** (ackley 0.666, rastrigin 16.225) | reproducible (Generator) | **confirmed** — beats fixed family(8) (which was 0.688) | +| 2026-06-27 | 4 | H8 | **SHIPPED**: `RacingTarget` + pluggable `selection="racing"` (modular registry) | real GPSAF run: pool shrinks 38→24→18→12→9; ~20-25% faster | equal quality | reproducible | **done** — `tests/test_selection.py` | +| 2026-06-27 | 5 | H3 | **SHIPPED**: `revalidate()` honors `nth_validate` (lazy re-selection). GPSAF Ackley(10), 220 evals, n_max_doe=200 | nth=1: 42s → **nth=5: 11s (~3.8×)**; racing+nth=5: **10.5s (~4×)** | best_F equal/better (3.78 → 2.59) | reproducible | **done** — biggest single wall-time win; stacks with racing | +| 2026-06-27 | 6 | H2 | LOO-CV vs 5-fold as model selectors (ackley/rastrigin/sphere, 9-model pool, 5 seeds) | n/a (selection-quality study) | **5-fold picks better-generalizing model** (Ackley gap 0.0001 vs LOO 0.0246); disagree 20–40% | n/a | **REJECTED** — confirms field experience that LOO is less robust; keep 5-fold | +| 2026-06-27 | 7 | H3+H8 on BO | route BO's model selection through the shared Target/RacingTarget + revalidate (measured: selection = **90%** of BO runtime, acquisition only 10%) | BO model_selection=True, Sphere(10), 50 gens: **179s → 42s (~4.3×)** | best_F equal/better (0.084 → 0.041) | reproducible (shared machinery) | **done** — BO reuses the same machinery, no duplicated racing code | + +--- + +## How to run + +```bash +# the harness (cost vs generalization + determinism check) +pyclawd python .claude/docs/model_selection_bench.py + +# add a new strategy: edit model_selection_bench.py (strat_* / POOLS), re-run, +# then append a row to the Results log above. +``` + +When a hypothesis is confirmed *and* you decide to ship it, make the change in +`src/pysamoo/**`, add a regression/golden test, and run `pyclawd check`. + +--- + +## Notes for the next session + +- H1 confirmed on single-objective; **next**: rerun H1 on a multi-objective problem + (e.g. `zdt1`) and on constraint pools (`DEFAULT_IEQ_CONSTR_MODELS` has 36) — those + pools are even larger and likely have the same redundancy. +- H5 **DONE**. The whole run is now reproducible: `self.random_state` is threaded + through every stochastic site in `gpsaf.py` (tournament/alpha/beta/restart), + `psaf.py`, `ssansga2.py` (roulette selection), `knockout.noisy`, the DOE sampling + (`algorithm.py` `_initialize_infill`), pymoo's `compare`/`RouletteWheelSelection` + (which accept `random_state`); CV folds are deterministic (`randomize=False`) and + the tie-break is `models[0]`. **Best practice followed: thread the Generator, never + seed globals.** Guarded by `tests/test_reproducibility.py`. Note: this enables a + same-machine full-run golden if ever wanted, but a *fixed-value* golden may differ + across BLAS/platforms because GP model selection can flip on float noise — the + equality-across-runs test is the robust guard. +- H2 (closed-form LOO) is the highest-impact algorithmic change but needs care: + pysamoo's RBF defaults are interpolating (H_ii = 1 ⇒ PRESS degenerates), so LOO + applies cleanly to Kriging/GP and *regularized* RBF only. diff --git a/.claude/docs/model-selection-research.md b/.claude/docs/model-selection-research.md new file mode 100644 index 0000000..ceec274 --- /dev/null +++ b/.claude/docs/model-selection-research.md @@ -0,0 +1,190 @@ +# Research dossier: faster, reproducible surrogate model selection in pysamoo + +*Companion to [model-selection-loop.md](model-selection-loop.md). Experiment harness: +[model_selection_bench.py](model_selection_bench.py).* + +## 1. Why this matters + +pysamoo is a **generic** surrogate-assisted optimizer: a pymoo algorithm wrapped so +that, each infill iteration, it fits surrogate models to the evaluated archive and +optimizes *those* instead of the expensive true problem. The framework "only needs +models" — so the quality of the **model-selection** step (which model to trust) is +the heart of the method. + +Two problems make that step the dominant pain point: + +- **Cost.** Profiling earlier put model fitting at ~**87 %** of runtime; a single + selection step grows from ~3 s to **>100 s** as the archive grows. +- **Reproducibility.** Repeated runs with a *fixed* pymoo seed produce different + results — which blocks golden tests and makes science hard. + +> **The framing that drives this study (the key insight):** model selection is not +> about fitting the current points well — it is about **generalization**: choosing a +> model that predicts well on *unseen* points. Any speed-up must therefore be judged +> on **out-of-sample accuracy**, never on training fit or runtime alone. Cutting cost +> while preserving generalization is the whole game. + +## 2. How model selection actually works today (code facts) + +Sources: `src/pysamoo/core/{target,defaults,surrogate,algorithm}.py`, +`ezmodel/core/{partitioning,benchmark}.py`, `ezmodel/util/partitioning/crossvalidation.py`. + +- **The pool is large.** `DEFAULT_OBJ_MODELS` builds **38 candidates per objective** + (32 RBF = kernel{linear,cubic,gaussian,mq} × tail{const,lin,quad,lin+quad} × + normalized{T,F}, plus 6 Kriging = {const,lin,quad}×{plain,ARD}). Constraints add + 36 (ineq) / 21 (eq) more — *per constraint*. +- **Selection = k-fold CV over the whole pool.** `Target.validate` runs a + `Benchmark` over all models with **5-fold** CV, scores each fold, and + `find_best` ranks lexicographically by `[kendall_tau, mae]` (mean over a rolling + window of the last 5 validations). ⇒ **5 × 38 = 190 model fits per objective per + selection call** (serial; the single-train/test path costs 38). +- **It runs every iteration.** `find_best=True` is the default on every `validate`. + GPSAF/PSAF/SSANSGA2 call `validate` at init (CV) and again every `_advance` + (single split). **`nth_validate` (the intended "re-select only every N iters" + throttle) is defined but never read — dead code.** This is the biggest structural + inefficiency: the pool is re-benchmarked and the winner re-chosen constantly. +- **Cost drivers (ranked):** (1) DACE **Kriging** boxmin hyperparameter optimization + (ARD optimizes one θ per dimension); (2) **GP O(n³)** in archive size n; + (3) **pool × folds** = 190 fits; (4) RBF SVD solves (cheap by comparison). + +## 3. Why runs were non-reproducible (root cause — now FIXED) + +> **Status (2026-06-27): resolved.** `self.random_state` is now threaded through +> every stochastic site (DOE sampling, GPSAF/PSAF/SSANSGA2 infill, `knockout.noisy`, +> pymoo `compare`/`RouletteWheelSelection`), CV folds are deterministic +> (`randomize=False`), and the tie-break is `models[0]`. All three algorithms are +> bit-reproducible under a fixed seed; guarded by `tests/test_reproducibility.py`. +> Best practice applied: thread the `Generator`, never seed globals (NumPy's +> guidance). The historical analysis below is retained for context. + + +pymoo 0.6.1 changed seeding: `Algorithm.setup` now creates a **local** +`np.random.default_rng(seed)` and **no longer seeds the global `np.random`/`random`**. +pysamoo's selection/infill code still uses the **global** RNGs, which are therefore +left at OS-entropy state and drift between runs. The two selection-specific sites: + +1. **CV fold shuffle is unseeded** — `target.py:73` constructs + `CrossvalidationPartitioning(self.n_folds)` with **no seed**, and + `crossvalidation.py` does `random.shuffle(indices)` on the global `random`. + Different folds ⇒ different CV scores ⇒ different winner. +2. **Random tie-break** — `target.py:119` ends `find_best` with + `np.random.choice(models)`. Because `kendall_tau` returns an **integer** disorder + count, ties are *frequent*, so this fires often and flips the choice. + +Float noise from threaded BLAS (`np.linalg.svd`, DACE) can also flip near-ties and +route into the unseeded tie-break. **Verified:** with unseeded folds the selected +model flips across perturbed ambient states; **seeding the folds makes it +deterministic** (`model_selection_bench.py`, determinism section). + +## 4. Measured cost vs generalization (the central trade-off) + +From `model_selection_bench.py` (5-var problems, held-out test set of 400 points): + +| problem | pool | sel time | **test RMSE** | test τ | picked | +|---|---|---:|---:|---:|---| +| ackley | full (38) | 0.78 s | **0.80** | 0.51 | RBF mq | +| ackley | small (3) | 0.02 s | **33.5** ❌ | -0.04 | RBF cubic | +| ackley | **family (8)** | **0.07 s** | **0.80** ✅ | 0.51 | RBF mq | +| ackley(n=80) | full (38) | 1.41 s | 0.70 | 0.58 | RBF mq | +| ackley(n=80) | **family (8)** | **0.12 s** | **0.69** ✅ | 0.58 | RBF mq | +| rastrigin | full (38) | 0.73 s | 18.8 | 0.42 | RBF mq | +| rastrigin | small (3) | 0.02 s | 148.8 ❌ | 0.15 | RBF cubic | +| rastrigin | **family (8)** | **0.07 s** | **18.5** ✅ | 0.43 | RBF lin | + +**Findings** +- **Naively shrinking the pool destroys generalization** (small(3): RMSE 4–170× + worse). The cheap pool simply lacked the kernel family that fits the function. +- **But a redundancy-free pool keeps it.** `family(8)` (one model per kernel family, + ±normalization) matches full(38) generalization at **~10× lower cost**. The 38-pool + spends most of its budget on near-duplicate RBF tail/normalization variants that + rarely win. +- ⇒ The win is **cover the model families, drop the redundancy** — not "use fewer + models" blindly. + +## 5. What the literature says (cited) + +Full survey with URLs in the agent report; the load-bearing techniques: + +1. **Closed-form LOO-CV for GP/Kriging** (Rasmussen & Williams, *GPML* §5.4.2, + eq. 5.12): leave-one-out mean/variance from the *one* factorization the GP already + computes — `μ_i = y_i − [K⁻¹y]_i / [K⁻¹]_ii`, `σ²_i = 1/[K⁻¹]_ii` — total overhead + O(n²), **no k-fold refitting**. Rank by **log pseudo-likelihood** (eq. 5.11), not + squared error. RBF/kernel-ridge analogue: PRESS via the hat matrix, + `resid_i/(1−H_ii)`, and GCV (Golub–Heath–Wahba 1979). + https://gaussianprocess.org/gpml/chapters/RW5.pdf + > ⚠️ **Tested and REJECTED for *model selection* here** (`loo_vs_kfold.py`, H2). LOO + > as a *selector* is less robust than 5-fold: it picks a worse-generalizing model + > (Ackley test-gap 0.025 vs 0.0001) and disagrees with 5-fold on 20–40% of seeds. + > LOO's low estimator-bias does not translate to good selection — training on n−1 + > points makes all candidates look alike, so the pick is swayed by single points. + > Keep 5-fold. (LOO would still be fine as a *cheap fit diagnostic*, just not to + > choose among models.) Also: ``pydacefit`` Kriging `boxmin` intermittently raises + > under numpy 2 (`nonzero on 0d arrays`) — a real bug in the dependency stack that + > silently drops Kriging candidates from selection; worth fixing in ezmodel/pydacefit. +2. **Lazy / periodic re-selection** — decouple selection frequency from infill + frequency; re-select every k iters or on a trust-region degradation trigger. + Evidence the quality cost is small: Ahrari & Verstraete, *SWEVO* 2023; + Hanawa et al. 2025. *This is exactly what the dead `nth_validate` was meant to do.* +3. **Cap the GP training set** to an L-nearest / most-recent subset ⇒ O(L³), + constant in archive size, often *more* accurate where search is active + (GPEME, Liu et al. 2014; TuRBO, Eriksson et al. 2019). +4. **Stop electing a single winner** — PRESS-weighted ensemble reuses the LOO + residuals, removes winner-flip variance, gives free uncertainty (Goel et al. 2007; + HeE-MOEA, Guo et al. 2019). +5. **Racing / successive-halving** over the pool (resource = folds/subsample) for any + candidate lacking a closed-form LOO (Hoeffding races, Maron & Moore 1993; + Successive Halving, Jamieson & Talwalkar 2016). +6. **Determinism**: seed folds once per iteration and reuse for all candidates; + fixed pool order + deterministic argmin tie-break; seed GP restarts + (scikit-learn common-pitfalls guidance). *LOO (item 1) sidesteps fold randomness + entirely.* + +## 6. Synthesis → what to try (feeds the loop) + +The independent cost angles compose: **redundancy-free pool** (×10, measured) × +**closed-form LOO instead of k-fold** (×k, removes refits) × **cap the fit set** +(bounds O(n³)) × **lazy re-selection** (×k in frequency). Determinism comes for free +from LOO, or from seeding folds + a deterministic tie-break. Each is a hypothesis in +the loop, ranked by impact-per-effort there. + +## 7. Proposed method: the adaptive racing pool (H8) + +The most promising *intelligent* selection — it reduces the candidate pool over time +from evidence, rather than by a fixed guess. Prototype + numbers: +[adaptive_pool_prototype.py](adaptive_pool_prototype.py). + +**Algorithm.** Keep an `active` set (initially the full pool) and a rolling window of +each model's recent CV error. + +1. **Score** only the *active* models each iteration (cost shrinks as the set shrinks). +2. **Prune** after a short warmup: rank active models by rolling-mean CV error, keep + the top `keep_ratio` (successive-halving style), **down to a floor** — and always + retain **≥1 model per kernel family** (the diversity floor that protects + generalization; this is why naive pool-cutting failed in §4). +3. **Re-admit** a round-robin batch of pruned models every `R` iterations, so a model + that becomes good only on a larger archive can return (handles non-stationarity; + this is the answer to "won't it lock in and never switch?"). + +**Why it is well-suited to this problem.** GP fitting is O(n³) and the archive grows, +so the pool is *full while fits are cheap* (small n) and *shrinks exactly as fits get +expensive* (large n). The wall-clock saving is therefore larger than the fit-count +saving. + +**Measured (20-iteration simulated run, growing archive):** + +| | full pool | fixed family(8) | **racing** | +|---|---:|---:|---:| +| Ackley — total model-fits | 2700 | 800 | **1510 (56%)** | +| Ackley — mean held-out RMSE | 0.666 | 0.688 | **0.666** (= full) | +| Rastrigin — total model-fits | 2700 | 800 | **1515 (56%)** | +| Rastrigin — mean held-out RMSE | 16.225 | 16.259 | **16.225** (= full) | + +Racing matched the full pool's generalization **exactly** while doing ~half the fits — +and beat the fixed family pool, because it *adapts which models to keep* instead of +guessing up front. Active set shrank 27 → 18 → 12 → 10 with periodic re-admission. + +**Implementation path (not yet shipped):** add a `RacingTarget` (a `Target` subclass +that maintains the active set + rolling history and overrides `validate` to score only +the active set) selectable via a config flag; combine with closed-form LOO (H2) and +lazy re-selection (H3) for compounding gains. Reproducible by construction (uses the +threaded `random_state`). diff --git a/.claude/docs/model_selection_bench.py b/.claude/docs/model_selection_bench.py new file mode 100644 index 0000000..b72414b --- /dev/null +++ b/.claude/docs/model_selection_bench.py @@ -0,0 +1,160 @@ +"""Experiment harness for the model-selection research loop. + +Measures, for a given *selection strategy*, the three quantities the loop trades +off (see model-selection-loop.md): + + 1. cost — wall-clock seconds to select a model, + 2. generalization — out-of-sample accuracy of the selected model on a held-out + test set (RMSE and rank correlation), the property that + model selection exists to protect, + 3. determinism — whether the selection is identical under a perturbed ambient + RNG state (the failure mode that makes full runs irreproducible). + +Run: pyclawd python .claude/docs/model_selection_bench.py + +A "strategy" is just a callable that, given a training Population and a Target +factory, returns (selected_label, fitted_model, seconds). New loop iterations add +strategies here and append their numbers to the results log in the loop doc. +""" + +import random +import time + +import numpy as np +from pymoo.core.population import Population +from pymoo.problems import get_problem +from pymoo.util.normalization import NoNormalization + +from ezmodel.models.kriging import Kriging +from ezmodel.models.rbf import RBF +from pysamoo.core.defaults import DEFAULT_OBJ_MODELS +from pysamoo.core.target import Target + +# --- model pools ----------------------------------------------------------------- + + +def pool_full(**d): + """The production pool: 38 candidates (32 RBF + 6 Kriging).""" + return DEFAULT_OBJ_MODELS(**d) + + +def pool_small(**d): + """A hand-picked 3-model pool (cheap but narrow).""" + return { + "rbf-cubic": RBF(kernel="cubic", **d), + "rbf-cubic-norm": RBF(kernel="cubic", normalized=True, **d), + "kriging-lin": Kriging(regr="linear"), + } + + +def pool_one_per_family(**d): + """One representative per kernel family + one Kriging (8 candidates).""" + p = {} + for k in ["linear", "cubic", "gaussian", "mq"]: + p[f"rbf-{k}"] = RBF(kernel=k, **d) + p[f"rbf-{k}-norm"] = RBF(kernel=k, normalized=True, **d) + return p + + +POOLS = {"full(38)": pool_full, "small(3)": pool_small, "family(8)": pool_one_per_family} + + +# --- strategies ------------------------------------------------------------------ + + +def strat_kfold(pool_factory, n_folds=5, seed_folds=None): + """Current pysamoo behaviour: k-fold CV over the whole pool, pick best. + + With ``seed_folds`` set, the global RNG is seeded before validation so the + CV partition (and hence the choice) is reproducible. + """ + + def run(pop): + if seed_folds is not None: + random.seed(seed_folds) + np.random.seed(seed_folds) + t = Target(("F", 0), pool_factory(norm_X=NoNormalization()), n_folds=n_folds) + t0 = time.perf_counter() + t.validate(pop) + dt = time.perf_counter() - t0 + t.fit(pop) + return t.best, t.obj, dt + + return run + + +# --- metrics --------------------------------------------------------------------- + + +def kendall_tau(a, b): + """Fraction of concordant pairs minus discordant (rank agreement, in [-1, 1]).""" + n = len(a) + c = d = 0 + for i in range(n): + for j in range(i + 1, n): + s = np.sign(a[i] - a[j]) * np.sign(b[i] - b[j]) + if s > 0: + c += 1 + elif s < 0: + d += 1 + return (c - d) / (c + d) if (c + d) else 0.0 + + +def evaluate(strategy, problem, n_train, n_test=400, seed=0): + """Run a strategy and score cost + generalization on held-out points.""" + rng = np.random.RandomState(seed) + xl, xu = problem.bounds() + Xtr = rng.rand(n_train, problem.n_var) * (xu - xl) + xl + ytr = problem.evaluate(Xtr).reshape(-1, 1) + Xte = rng.rand(n_test, problem.n_var) * (xu - xl) + xl + yte = problem.evaluate(Xte).ravel() + + pop = Population.new(X=Xtr, F=ytr) + label, model, dt = strategy(pop) + yhat = model.predict(Xte).ravel() + rmse = float(np.sqrt(np.mean((yhat - yte) ** 2))) + tau = kendall_tau(yte[:120], yhat[:120]) # rank fidelity on a subset (cheap) + return dict(label=label, seconds=dt, rmse=rmse, tau=tau) + + +def determinism_check(make_strategy, problem, n_train=60, trials=3): + """A strategy is deterministic if the selected model is identical across trials + that start from *different* ambient RNG states (mimicking a real run).""" + labels = [] + for k in range(trials): + # perturb ambient state the way a real optimization loop would + np.random.seed(); random.seed() + [np.random.rand() for _ in range(k * 7 + 1)] + res = evaluate(make_strategy(), problem, n_train) + labels.append(res["label"]) + return len(set(labels)) == 1, labels + + +# --- main ------------------------------------------------------------------------ + +if __name__ == "__main__": + problems = [("ackley", get_problem("ackley", n_var=5)), ("rastrigin", get_problem("rastrigin", n_var=5))] + + print("=" * 92) + print("COST vs GENERALIZATION (lower rmse = better generalization; tau closer to 1 = better ranking)") + print("=" * 92) + header = f"{'problem':10s} {'pool':10s} {'n_train':>7s} {'sel_s':>8s} {'test_rmse':>10s} {'test_tau':>9s} picked" + print(header) + print("-" * 92) + for pname, prob in problems: + for n in [40, 80]: + for poolname, pool in POOLS.items(): + r = evaluate(strat_kfold(pool, n_folds=5), prob, n) + print( + f"{pname:10s} {poolname:10s} {n:7d} {r['seconds']:8.2f} {r['rmse']:10.3f} {r['tau']:9.3f} {r['label']}" + ) + print("-" * 92) + + print("\n" + "=" * 92) + print("DETERMINISM (selected model identical across perturbed ambient RNG states?)") + print("=" * 92) + prob = get_problem("ackley", n_var=5) + ok_unseeded, labels_u = determinism_check(lambda: strat_kfold(pool_full, n_folds=5), prob) + ok_seeded, labels_s = determinism_check(lambda: strat_kfold(pool_full, n_folds=5, seed_folds=1), prob) + print(f"unseeded folds : deterministic={ok_unseeded} picks={labels_u}") + print(f"seeded folds : deterministic={ok_seeded} picks={labels_s}") diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8a8fb9b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,57 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + check: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + # The surrogate stack (pysurrogate/pydacefit/pysampling/ezmodel) is not yet released to PyPI + # with the APIs this branch needs (e.g. pydacefit.regr.ConstantRegression), so install the + # required states from git. TODO: pin to released versions once the stack is published. + - name: Install unreleased surrogate dependencies from git + run: | + python -m pip install --upgrade pip + pip install "git+https://github.com/anyoptimization/pysampling" + pip install "git+https://github.com/anyoptimization/ezmodel@main" + pip install "git+https://github.com/anyoptimization/pysurrogate@feat/fully-bayesian-gp" + pip install --force-reinstall --no-deps "git+https://github.com/msu-coinlab/pydacefit@main" + + - name: Install package and dev tools + run: | + pip install -e ".[dev]" + + - name: Lint (ruff) + run: ruff check + + - name: Format check (ruff) + run: ruff format --check --quiet + + - name: Type check (mypy) + run: mypy + + - name: Tests (unit tiers, excludes slow and golden) + env: + MPLBACKEND: Agg + run: pytest -m "not slow and not golden" + + - name: Golden baselines (its own gate, excludes slow) + env: + MPLBACKEND: Agg + run: pytest -m "golden and not slow" diff --git a/.pyclawd/config.py b/.pyclawd/config.py new file mode 100644 index 0000000..b0fd4db --- /dev/null +++ b/.pyclawd/config.py @@ -0,0 +1,70 @@ +"""pysamoo's pyclawd config — drives `pyclawd test/lint/typecheck/...` for this repo. +""" + +from pyclawd import ( + DescriptionConfig, + DocsConfig, + DoctorConfig, + GoldenConfig, + Project, + QualityConfig, + TestConfig, +) + +project = Project( + name='pysamoo', + conda_env='default', + root_markers=["pyproject.toml", "setup.py"], + # The pyclawd this config was built on. `pyclawd doctor` WARNs if the + # running pyclawd has drifted to a different minor (migration may be needed). + pyclawd_version='0.1.0', + # Default directory `pyclawd ls` lists (the code/source root). + src_dir="src", + quality=QualityConfig( + lint_cmd=["ruff", "check"], + lint_fix_cmd=["ruff", "check", "--fix"], + format_cmd=["ruff", "format"], + format_check_cmd=["ruff", "format", "--check", "--quiet"], + typecheck_cmd=["mypy"], + check_sequence=["format-check", "lint", "typecheck", "descriptions", "test"], + ), + descriptions=DescriptionConfig( + # vendored third-party code, runnable examples, and WIP experiments are + # not held to the module-description bar. + exclude=[r"/vendor/", r"/usage/", r"/experimental/"], + ), + test=TestConfig( + tests_dir='tests', + classname_prefix="tests.", + integration_files=[], + # golden is its own gate (`pyclawd golden`), so exclude it from the unit tiers -- an + # intended-change session should not fail `pyclawd test` on drift a human has yet to bless. + markers={ + "fast": "not slow and not integration and not golden", + "default": "not slow and not golden", + "all": "", + }, + ), + golden=GoldenConfig( + # Slightly looser than the default 1e-9 to tolerate BLAS/platform float + # noise in the numerical kernels across machines. + rtol=1e-7, + atol=1e-10, + ), + docs=DocsConfig( + # A thin runner maps `pyclawd docs ` onto a Sphinx build of the single, + # pre-executed index.ipynb (nbsphinx reuses stored outputs; no re-execution). + runner=["python", "docs/runner.py"], + source_dir="docs/source", + build_html="docs/build/html", + ), + doctor=DoctorConfig( + core_deps=["pymoo", "ezmodel"], + dev_deps=["pytest", "pytest-xdist", "pytest-cov", "sphinx", "nbsphinx", "sphinx_rtd_theme"], + tool_files=[], + binaries=[ + ("ruff", "pip install ruff"), + ("mypy", "pip install mypy"), + ], + ), +) diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9e22f4b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,164 @@ +# AGENTS.md — working in pysamoo + +**pysamoo** is driven by [pyclawd](https://github.com/julian/pyclawd), a +config-driven Python dev-task CLI: one file (`.pyclawd/config.py`) describes the +project and `pyclawd ` is the single contract for every task — humans and AI +agents drive it the same way. + +This file is the **operational contract** — the commands, the boundaries, the +non-negotiables. It is always in your context, so follow it. For the *why* behind +the rules — the testing taxonomy, typing, packaging, and docstring doctrine, with +examples — invoke the **`pyclawd` skill** (Claude Code; installed user-scope and +shared across every pyclawd project). In short: **AGENTS.md is what to run; the +skill is how to write good code.** + +## Critical rule — how to run Python + +**ALWAYS run Python through `pyclawd python`. NEVER call bare `python` / `python -c`.** + +```bash +pyclawd python script.py # run a script +pyclawd python -m pytest ... # run a module +pyclawd python -c "import pysamoo" # quick check +``` + +`pyclawd python` runs in the project's configured env (the `conda_env` in +`.pyclawd/config.py`, or whatever env pyclawd is installed into) with the repo +root on `PYTHONPATH`. Bare `python` misses the env and the in-tree source. + +## Commands — quick reference + +| Task | Command | +|---|---| +| Resolved config (what each command runs) | `pyclawd config` | +| Health-check the dev env | `pyclawd doctor` | +| Run Python in the env | `pyclawd python ` · `-m ` · `-c ` | +| Fast smoke (<30s, no integration) | `pyclawd test fast` | +| Default gate (no slow) | `pyclawd test run` | +| Full suite (incl. slow) | `pyclawd test all` | +| Select tests | `pyclawd test -k ` · `pyclawd test tests/path::node` | +| Fix-loop | `pyclawd test failures` → `pyclawd test fix` → `pyclawd test run` | +| Coverage | `pyclawd coverage [--check] [--html]` | +| Prove behavior unchanged | `pyclawd golden [-k EXPR]` · `golden update [-k EXPR]` · `status` · `prune` · `vendor ` | +| Lint / autofix | `pyclawd lint` · `pyclawd lint --fix` · `pyclawd lint ` | +| Format / check | `pyclawd format` · `pyclawd format --check` · `pyclawd format ` | +| Type-check | `pyclawd typecheck` · `pyclawd typecheck ` | +| **Aggregate quality gate** | `pyclawd check` · `--fix` · `--skip ` · `--fail-fast` · `pyclawd check ` | +| Build / dist / clean | `pyclawd compile` · `pyclawd dist` · `pyclawd clean [--ext]` | +| Docs (if configured) | `pyclawd docs build\|run\|render\|serve\|status\|failures\|exec ` | +| Code map (file → description) | `pyclawd ls [DIR]` · `pyclawd ls --missing` | +| Manage agent skills | `pyclawd skills list` · `pyclawd skills install` | +| Version + config drift | `pyclawd version` · `pyclawd version --json` | +| What changed (since config) | `pyclawd changelog [--since V] [--full]` | +| Repo root | `pyclawd root` | + +Run `pyclawd config` first — it shows the exact command every verb resolves to and +the `PYCLAWD_*` override knobs (`PYCLAWD_CONFIG`, `PYCLAWD_DISCOVERY`, +`PYCLAWD_PYTHON`, `PYCLAWD_WORK_DIR`). To use pyclawd **without committing** a +`.pyclawd/` folder, set `PYCLAWD_DISCOVERY=".local/.pyclawd:.pyclawd"` (relative → +safe globally) and keep a gitignored `/.local/.pyclawd/config.py`. +`pyclawd check` runs all quality steps (format-check +→ lint → typecheck) **regardless of individual failures**, streaming output inline, +then runs **test** only if quality passed. Use `--skip ` (repeatable) to omit +a step, `--fail-fast` to stop at the first failure, `--fix` to apply format+lint +autofixes in place, and `--log` to also write each step's output to a file (CI +artifacts). Build/dist/clean and docs commands only do real work when the project +configures them; otherwise they degrade gracefully (exit 2 = not configured). + +## Test tiers + +| Tier | Marker filter | When | +|---|---|---| +| `fast` | `not slow and not integration` | After every edit — <30s smoke | +| `run` (default) | `not slow` | Before opening a PR | +| `all` | _(no filter)_ | Nightly / pre-release | + +Mark slow tests `@pytest.mark.slow`, tests needing live services +`@pytest.mark.integration`. Unmarked tests run in every tier — never mark a test +`fast`. The fix-loop and failure taxonomy live in the `pyclawd` skill's `references/tests.md`. + +## Behavior oracle (golden) + +`pyclawd check` proves code **clean** (format/lint/typecheck/test); it cannot prove +behavior **unchanged** — a clean edit can still move a number. `pyclawd golden` +closes that gap: it compares observable outputs against **committed** baselines and +fails on drift (tolerance is the gate; the stored hash is only a fast path, so +baselines survive cross-platform float jitter; values are inline so `git diff` shows +`0.925 → 0.522`). Workflow: **agents compare, humans bless** — `pyclawd golden` +gates, `pyclawd golden update [-k EXPR]` records an *intended* change (merges, never +wipes others), then a human reviews the baseline `git diff` and commits; `status` +lists snapshots, `prune` drops orphaned ones. Write a golden test by tagging it +`@pytest.mark.golden` and **`return`ing the value** to snapshot — the pytest plugin +captures it (there is no fixture). It works **standalone in a bare-pytest repo with +zero pyclawd references**; `pyclawd golden` is the optional CLI wrapper (`GoldenConfig` +drives it; unset → exit 2). For zero pyclawd dependency, `pyclawd golden vendor ` +copies the plugin into one self-contained file. golden is a **separate tier** — +exclude it from the unit tiers (`"default": "not slow and not golden"`) and run it as +its own gate. Full doctrine in the **`pyclawd-golden`** skill. + +## Architecture — generic core + per-project config + +pyclawd ships a project-agnostic command layer. Everything project-specific lives +in **`.pyclawd/config.py`** — a module-level `project = Project(...)` (from +`pyclawd import Project`). The directory containing `.pyclawd/` **is** the repo +root. The `Project` model groups config: `QualityConfig` (lint/format/typecheck), +`TestConfig` (tests dir + tier markers), `DocsConfig` (or `None`), `DoctorConfig` +(deps/binaries to probe). Read `.pyclawd/config.py` before assuming how this +project is wired — it is the single source of truth for env, paths, and checks. + +**Every module opens with a one-line docstring** — the `descriptions` step of +`pyclawd check` enforces this for `DescriptionConfig`-included files; `pyclawd ls +--missing` is the broader exploratory view of all files lacking one. **Docstrings use Google style, no types** (`Args:` / `Returns:` +/ `Raises:` with plain descriptions — annotations carry the types). `pyclawd lint` +checks docstring style via ruff's `D` rules (Google convention) — write `Args:`/ +`Returns:`, not NumPy `Parameters`/`----------`. The `pyclawd` skill has examples +and shows how to change the convention when adopting an existing repo. + +## Boundaries + +### Always +- Run code via `pyclawd python` — never bare `python`. +- Run `pyclawd doctor` first when the env looks off or tests fail to import. +- **Run `pyclawd check` before declaring work done** or opening a PR. +- Fix the **cause** of a failing test, not the assertion — tolerances for floats, + pinned seeds for stochastic tests. +- Match existing patterns; read `.pyclawd/config.py` before changing wiring. + +### Ask first +- Destructive cleans (`pyclawd clean --ext`), committing, pushing, opening PRs. +- Changing `.pyclawd/config.py`, dependencies, or the public API surface. + +### Never +- Never call bare `python`/`pip` outside the project env. +- Never commit secrets, tokens, or credentials. +- Never weaken or delete a test to make a suite pass. +- Never leave the tree with a failing `pyclawd check`. +- Never use `git commit --no-verify` to bypass pre-commit hooks — fix the cause. +- Never wire `pyclawd golden update` into an autonomous loop — agents compare, + humans bless. + +## How you know you're done + +- `pyclawd check` is green (format-check, lint, typecheck, descriptions, and tests + all ✓). The `descriptions` step is the enforced gate — it passes when every + `DescriptionConfig`-included file (default `.py`/`.pyx`) has a one-line + description. +- `pyclawd doctor` exits 0 — no FAILs. +- Behavior is verified by tests, not just by inspection. + +`pyclawd ls --missing` is the broader **exploratory** view — it lists every repo +file lacking a one-liner, including templates/Markdown the `descriptions` gate +ignores, so it may be non-empty even when `pyclawd check` is green. + +--- + +**Going deeper.** This file is the contract. For the doctrine behind it, invoke the +Claude Code skills: **`pyclawd`** is the umbrella router — a lean overview plus +on-demand reference docs (`references/mental-model.md`, `references/tests.md`, +`references/quality.md`, `references/docs.md`, `references/packaging.md`) that carry +the testing, quality, docs, and packaging doctrine. Three focused standalone skills +sit alongside it: **`pyclawd-golden`** (the behavior oracle), **`pyclawd-doctor`** +(diagnose a broken env), and **`pyclawd-upgrade`** (migrate this project after pyclawd +itself is updated, when `pyclawd version` shows config drift). They are generic (not +specific to this repo) and update centrally when pyclawd is upgraded — which is exactly +why the deep doctrine lives there and not duplicated into this file. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/LICENSE b/LICENSE index be3f7b2..8ab7786 100644 --- a/LICENSE +++ b/LICENSE @@ -1,661 +1,75 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. +# PolyForm Noncommercial License 1.0.0 + + + +Required Notice: Copyright 2021-2026 Julian Blank + +## Acceptance + +In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses. + +## Copyright License + +The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license). + +## Distribution License + +The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license). + +## Notices + +You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example: + +> Required Notice: Copyright 2021-2026 Julian Blank + +## Changes and New Works License + +The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose. + +## Patent License + +The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software. + +## Noncommercial Purposes + +Any noncommercial purpose is a permitted purpose. + +## Personal Uses + +Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose. + +## Noncommercial Organizations + +Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding. + +## Fair Use + +You may have "fair use" rights for the software under the law. These terms do not limit them. + +## No Other Rights + +These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses. + +## Patent Defense + +If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. + +## Violations + +The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately. + +## No Liability + +***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.*** + +## Definitions + +The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms. + +**You** refers to the individual or entity agreeing to these terms. + +**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. + +**Your licenses** are all the licenses granted to you for the software under these terms. + +**Use** means anything you do with the software requiring one of your licenses. diff --git a/README.rst b/README.rst index 7ed9939..9e0b12f 100644 --- a/README.rst +++ b/README.rst @@ -14,6 +14,11 @@ pysamoo - Surrogate-Assisted Multi-objective Optimization The software documentation is available here: https://anyoptimization.com/projects/pysamoo/ +Developer guides (in this repository): + +- ``docs/PERFORMANCE.md`` — why surrogate-assisted runs can be slow and how to speed them up. +- ``docs/BENCHMARKING.md`` — measuring solution quality and developing better algorithms/models. + Installation ==================================================================== diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..ab4c68b --- /dev/null +++ b/conftest.py @@ -0,0 +1,21 @@ +"""Root pytest configuration. + +Golden behavior-regression tests (``@pytest.mark.golden``) are enforced by a +pytest plugin. Two environments must both work: + +* Local dev with pyclawd installed — pyclawd auto-registers its own golden plugin + via an entry point, so we must NOT also register the vendored copy (double + registration errors). +* CI / contributors without pyclawd — there is no entry-point plugin, so we load + the self-contained vendored copy (``tests/_golden_plugin.py``) instead. + +Regenerate the vendored plugin with ``pyclawd golden vendor tests/_golden_plugin.py``. +""" + +import importlib.util +import os + +_force_vendored = os.environ.get("PYSAMOO_FORCE_VENDORED_GOLDEN") == "1" + +if _force_vendored or importlib.util.find_spec("pyclawd") is None: + pytest_plugins = ["tests._golden_plugin"] diff --git a/docs/BENCHMARKING.md b/docs/BENCHMARKING.md new file mode 100644 index 0000000..a197311 --- /dev/null +++ b/docs/BENCHMARKING.md @@ -0,0 +1,102 @@ +# Benchmarking & Developing Better Methods + +pysamoo is, at its core, a **generic surrogate-assisted framework**: a pysamoo algorithm +wraps an ordinary pymoo algorithm and drives it with a **surrogate** built from pluggable +**models**. To develop a better method you therefore vary one of three things and *measure*: + +1. the **base algorithm** (NSGA2, NSGA3, GA, DE, …), +2. the **surrogate model(s)** (Kriging, RBF, your own ezmodel model), +3. the **strategy parameters** (`alpha`, `beta`, infill counts, archive caps). + +The `pysamoo.benchmark` package makes that measurement a few lines of code. + +## The harness in one minute + +```python +from pymoo.algorithms.moo.nsga2 import NSGA2 +from pymoo.problems.multi import ZDT1 +from pysamoo.algorithms.gpsaf import GPSAF +from pysamoo.benchmark import Scenario, ProblemSpec, run_benchmark, summarize, format_table + +problem = ZDT1(n_var=10) + +scenarios = [ + Scenario("NSGA2 (baseline)", lambda: NSGA2(pop_size=20, n_offsprings=10)), + Scenario("GPSAF", lambda: GPSAF(NSGA2(pop_size=20, n_offsprings=10), n_initial_doe=30)), +] +problems = [ProblemSpec("ZDT1", problem, n_evals=200)] + +records = run_benchmark(scenarios, problems, n_seeds=5) +print(format_table(summarize(records))) +``` + +- **`Scenario(name, factory)`** — `factory` is a zero-arg callable returning a *fresh* + algorithm (a new object per run, so seeds stay isolated). +- **`ProblemSpec(name, problem, n_evals)`** — a pymoo problem and its evaluation budget. +- **`run_benchmark(...)`** — runs every scenario × problem × seed and scores each run. +- **`summarize` / `format_table`** — aggregate to mean/std/best/time per pair. + +### The score (lower is better) + +| Problem type | Metric | Meaning | +|---|---|---| +| single-objective | `f_gap` | best feasible objective minus the known optimum | +| multi-objective (Pareto front known) | `igd` | inverted generational distance to the front | +| multi-objective (no front) | `hv_gap` | negative hypervolume vs an auto reference point | + +Infeasible/empty runs score `inf` and are flagged via `feasible_rate`. + +## Swapping the surrogate model — the "only needs models" idea + +`SurrogateAssistedAlgorithm` accepts a `surrogate=` object. `make_surrogate` builds one from +per-output **model-set factories**; each factory returns a `{name: model}` dict and the +surrogate cross-validates/selects among them automatically. + +```python +from ezmodel.models.rbf import RBF +from pysamoo.benchmark import make_surrogate + +def only_rbf(**defaults): + return {"rbf-cubic": RBF(kernel="cubic", **defaults)} + +scenarios = [ + Scenario("GPSAF (Kriging, default)", lambda: GPSAF(NSGA2(), n_initial_doe=30)), + Scenario("GPSAF (RBF only)", + lambda: GPSAF(NSGA2(), n_initial_doe=30, + surrogate=make_surrogate(problem, obj_models=only_rbf))), +] +``` + +To try a brand-new surrogate, implement an [ezmodel](https://pypi.org/project/ezmodel/) +model and return it from your factory — no changes to the algorithms are needed. + +## Example result + +`src/pysamoo/usage/usage_benchmark.py` (ZDT1, `n_var=10`, 200 evals, 3 seeds) produces, for +instance: + +``` +problem scenario metric mean std best time/run feas +------------------------------------------------------------------------------------- +ZDT1 GPSAF (Kriging) igd 0.0378 0.0044 0.0340 30.3s 100% +ZDT1 GPSAF (RBF only) igd 0.282 0.0866 0.166 2.0s 100% +ZDT1 NSGA2 (baseline) igd 0.846 0.149 0.645 0.0s 100% +``` + +Reading it: at this budget the surrogate is hugely beneficial (IGD 0.04 / 0.28 vs 0.85), and +the model choice is an accuracy/speed trade-off — Kriging is far more accurate here, RBF is +~15× faster per run. This is exactly the loop you iterate when developing a better method. +(At a tighter 60-eval budget the gap narrows: Kriging ≈ 0.16, RBF ≈ 0.29, baseline ≈ 0.88.) + +## Recommended workflow for algorithm development + +1. **Fix a budget and a problem set** representative of your target (single- and + multi-objective, constrained where relevant). +2. **Baseline first** — include the plain pymoo algorithm (no surrogate) as a scenario so you + can prove the surrogate actually helps at that budget. +3. **Vary one thing at a time** — model family, `alpha`/`beta`, infill count, archive cap. +4. **Use several seeds** (`n_seeds>=5`) and compare `mean ± std`, not a single run. +5. **Watch `time/run`** alongside quality — see [PERFORMANCE.md](PERFORMANCE.md) for the + cost model and speed-up levers. +6. **Lock in gains** — once a change helps, consider a `@pytest.mark.golden` baseline so a + later refactor can't silently regress the numbers. diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md new file mode 100644 index 0000000..73f530f --- /dev/null +++ b/docs/PERFORMANCE.md @@ -0,0 +1,173 @@ +# pysamoo Performance & Speed-up Guide + +This document explains **why surrogate-assisted runs can be slow**, gives **measured +numbers** for every algorithm, and lists **concrete levers to speed things up**. It +also records the dependency-version findings that affect runnability. + +> TL;DR +> - One iteration of every algorithm is **sub-second to ~1s**. Slowness comes from +> large *demo budgets* multiplied by Gaussian-Process (Kriging) fitting that scales +> **O(n³)** in the archive size. +> - The biggest single offender is **Bayesian Optimization with `model_selection=True`** +> (≈121 model fits per generation). Turning it off is a ~6–7× speedup. +> - Pick the surrogate to match the budget: **Kriging** is most accurate but expensive; +> **RBF** is far cheaper and often good enough. + +--- + +## 1. Where the time goes + +Surrogate-assisted algorithms repeat this loop: + +1. **Fit** a surrogate to all evaluated points (the *archive*). +2. **Optimize** the cheap surrogate (an inner evolutionary loop) to propose infills. +3. **Evaluate** the (few) infills on the true, expensive problem; append to the archive. + +Two costs dominate, both growing as the run proceeds: + +- **Surrogate fit — O(n³).** Kriging/GP fitting solves a dense `n × n` system (Cholesky / + least-squares) where `n` is the archive size. Doubling the archive ~8× the fit cost. + Over hundreds of infills the archive grows large and late iterations dominate the run. +- **Model selection — multiplicative.** When enabled, each fit step cross-validates a whole + *grid* of candidate models and refits the winner, multiplying the per-iteration fit count + (see §3). + +The acquisition / inner-optimization loop is usually secondary, but it is not free: it runs +a full evolutionary algorithm against the surrogate every iteration. + +--- + +## 2. Measured per-algorithm floor (one iteration) + +Initial DOE + ~1 infill, small problems, no plotting (`conda env default`, numpy 2.4.6, +pymoo 0.6.1.6): + +| Algorithm | DOE + 1 infill | +|---|---:| +| PSAF | 0.27 s | +| GPSAF (single-obj) | 0.58 s | +| SSANSGA2 (bi-obj) | 1.16 s | +| Bayesian Optimization (`n_gen=1`) | ~0 s (DOE only) | + +**Takeaway:** the algorithms themselves are cheap per step. The full demo scripts are slow +only because of their presentation budgets (hundreds of evaluations) and the O(n³) growth. + +### Full usage-example sweep (demo budgets, 180 s cap per script) + +Measured before the dependency fix in §5; "ran" = optimization completed. + +| Example | Wall time | Outcome | +|---|---:|---| +| `usage_gpsaf_constr.py` | 1.7 s | crashed early on `np.math` (now fixed, §5) | +| `usage_constr_sampling.py` | 5.3 s | ok | +| `usage_lqcmaes.py` | 6.6 s | ok | +| `usage_psaf.py` | 92 s | ok (full budget) | +| `usage_gpsaf_many.py` | 101 s | optimization ran; crashed at *plot* (`cm.get_cmap`, now fixed) | +| `usage_gpsaf_multi.py` | 129 s | optimization ran; crashed at *plot* (now fixed) | +| `usage_ssansga2.py` | 133 s | optimization ran; crashed at *plot* (now fixed) | +| `usage_gpsaf_single.py` | >180 s | timeout (budget) | +| `usage_gpsaf_cmoo.py` | >180 s | timeout (budget) | +| `usage_bayesian_optimization.py` | >180 s | timeout (see §3) | +| `usage_smac.py` | — | skipped (optional `smac` dep) | + +None of these are algorithm bugs: the failures were dependency drift (§5), and the timeouts +were budget size. This is why the test suite exercises **one iteration per algorithm** +instead of running the demo scripts verbatim (see `tests/test_usage.py`). + +--- + +## 3. Deep dive: Bayesian Optimization + +`usage_bayesian_optimization.py` runs `("n_gen", 150)` and never finishes within 180 s. + +**Root cause:** `BayesianOptimization(model_selection=True, ...)`. Every generation +(`src/pysamoo/experimental/bo.py`, `_infill`) expands the full Kriging hyperparameter grid +(regr × corr × thetaU × ARD = **24 configurations**), runs **5-fold cross-validation** over +all of them, then refits the winner — **24×5 + 1 = 121 Kriging fits per generation**. Profiling +shows `ModelSelection.do` accounts for **~87 %** of runtime. + +Each model-selection step also grows super-linearly with archive size: + +| archive size | one `ModelSelection.do` | +|---:|---:| +| 30 | 2.8 s | +| 90 | 10.7 s | +| 170 | >115 s | + +Per-generation cost, `n_var=10`: + +| archive | `model_selection=True` | `model_selection=False` | +|---:|---:|---:| +| 22 | 2.57 s | 0.31 s | +| 32 | 3.07 s | 0.52 s | + +So the 180 s budget is exhausted around generation ~35–40; the loop never approaches 150. + +**Fixes, ranked:** + +1. **`model_selection=False`** (`usage_bayesian_optimization.py`) — ~6–7× faster per + generation; falls back to a single `Kriging(regr="linear", corr="gauss", ARD=True)`. +2. **Reduce `n_gen`** 150 → ~50 — 150 sequential GP infills is unusually large for BO + (typical total budgets are 50–100); also keeps the archive (and O(n³) cost) small. +3. **Re-select only every k generations** (`bo.py:_infill`) — refit the chosen config in + between; removes ~120 of the 121 fits on most generations. +4. **Cap the GP archive** (`bo.py`, `self._archive.get(...)`) — fit on the best/nearest N + points (e.g. N=80) to bound per-fit cost regardless of run length. +5. **Shrink the hyperparameter grid** (`Kriging.hyperparameters()`) — fewer configs → fewer + CV fits. +6. **Cheaper acquisition search** (`robust_fmin_acquisition`, NicheGA pop/LHS sizes) — the + secondary ~13 %. + +--- + +## 4. Speed-up levers (all algorithms) + +| Lever | How | Effect | Trade-off | +|---|---|---|---| +| Smaller evaluation budget | `("n_evals", N)` / `("n_gen", N)` | Linear; also caps archive growth | Fewer evals → worse final solution | +| Cheaper surrogate | `surrogate=make_surrogate(problem, obj_models=only_rbf)` | RBF ≈ 6× faster than Kriging here | Lower model accuracy | +| Disable model selection | `model_selection=False` (BO) / smaller model dict | Removes the per-iter CV multiplier | No per-iteration model adaptation | +| Cap the archive used for fitting | subset best/nearest-N before `surrogate.fit` | Bounds the O(n³) term | GP ignores far/old points | +| Fewer infills per round | `n_max_infills` / `n_infills` smaller | Fewer surrogate-optimize loops | Slower convergence per eval | +| Smaller inner search | surrogate-side `pop_size` / `n_gen` | Cheaper step 2 | Weaker infill proposals | +| Skip plotting in batch runs | `matplotlib.use("Agg")`; don't call `.show()` | Removes GUI/render cost | None for headless runs | + +Rule of thumb: **match the surrogate to the budget.** Tiny budgets → RBF or a single Kriging +config. Generous budgets where each true evaluation is very expensive → Kriging with model +selection can pay off. + +--- + +## 5. Dependency version notes (runnability) + +Environment: **numpy 2.4.6**, **matplotlib 3.11.0**. + +`setup.py` originally pinned `pymoo==0.6.1.1`, which predates two upstream removals and +crashes against this environment: + +- `numpy>=1.25` removed `numpy.math`, still used by pymoo's ES code + (`pymoo/algorithms/soo/nonconvex/es.py`) → broke the ISRES-based `usage_gpsaf_constr.py`. +- `matplotlib>=3.9` removed `matplotlib.cm.get_cmap`, still used by pymoo's plotting → + broke the plot step of several examples (after the optimization had already run). + +Both are **pymoo-vs-dependency drift, not pysamoo bugs.** The pin is now +**`pymoo>=0.6.1.5,<0.6.2`** (resolves to 0.6.1.6), which replaced `np.math`→`math` and +`cm.get_cmap`→`pyplot.get_cmap` natively. A compatibility audit found every pymoo API +pysamoo imports is present and signature-compatible in 0.6.1.6, so the bump is **low risk**. +The only code touching changed/removed pymoo APIs lives in `src/pysamoo/experimental/` +(`SACOBRA.py`), which was already broken under 0.6.1.1 and is out of scope. + +If you ever need to run under an older pymoo, restore `np.math`/`cm.get_cmap` shims as in an +earlier version of `tests/conftest.py`. + +--- + +## 6. How this maps to the test suite + +`tests/test_usage.py` runs **one minimal iteration per algorithm** (and the sampling +routine) with no plotting — the whole suite is a few seconds and validates that every +algorithm wires up and produces finite results. The large, slow demo scripts remain under +`src/pysamoo/usage/` as illustrative examples, not as the test contract. + +See also [BENCHMARKING.md](BENCHMARKING.md) for measuring solution *quality* (not just +runtime) and for developing better algorithms/models. diff --git a/docs/runner.py b/docs/runner.py new file mode 100644 index 0000000..5fd7421 --- /dev/null +++ b/docs/runner.py @@ -0,0 +1,55 @@ +"""Docs runner for ``pyclawd docs`` — maps pyclawd's docs verbs onto a Sphinx build of ``docs/source``. + +``pyclawd docs`` is a thin orchestrator: it appends a sub-verb (``compile``/``run``/``all``/``build``/ +``exec``/``clean``) to this script and runs it. pysamoo's docs are a single, pre-executed +``index.ipynb`` rendered by nbsphinx, so ``compile``/``run`` are no-ops (there is nothing to generate +and the notebook's stored outputs are reused rather than re-executing the expensive algorithm cells); +the ``all``/``build``/``render`` verbs all shell out to ``sphinx-build``. +""" + +import shutil +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +SOURCE = ROOT / "source" +BUILD_DIR = ROOT / "build" +HTML = BUILD_DIR / "html" + + +def _sphinx_html() -> int: + """Render the HTML site from ``docs/source`` (nbsphinx reuses the notebook's stored outputs).""" + return subprocess.call([sys.executable, "-m", "sphinx", "-b", "html", str(SOURCE), str(HTML)]) + + +def _exec_page(page: str) -> int: + """Execute a single notebook in place and stream any error (``pyclawd docs exec ``).""" + nb = SOURCE / page + if not nb.exists(): + print(f"docs exec: no such page {nb}", file=sys.stderr) + return 2 + return subprocess.call( + [sys.executable, "-m", "jupyter", "nbconvert", "--to", "notebook", "--execute", "--inplace", str(nb)] + ) + + +def main(argv: list) -> int: + verb = argv[0] if argv else "all" + rest = argv[1:] + if verb in ("compile", "run"): + return 0 # single pre-executed notebook: nothing to compile or run + if verb in ("all", "build", "render"): + return _sphinx_html() # --continue / --fast accepted and ignored (one page, stored outputs) + if verb == "exec": + return _exec_page(rest[0]) if rest else 2 + if verb == "clean": + if BUILD_DIR.exists(): + shutil.rmtree(BUILD_DIR) + return 0 + print(f"docs runner: unknown verb {verb!r}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/docs/source/conf.py b/docs/source/conf.py index a9b5bd0..508bad6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -9,20 +9,23 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) +import os +import sys + +sys.path.insert(0, os.path.abspath("../../src")) # -- Project information ----------------------------------------------------- project = 'pysamoo' -copyright = '2022, Julian Blank' +copyright = '2022-2026, Julian Blank' author = 'Julian Blank' -# The full version, including alpha/beta/rc tags -release = '0.1' +# The full version, kept in sync with the package (single source of truth). +from pysamoo.version import __version__ # noqa: E402 + +version = __version__ +release = __version__ # -- General configuration --------------------------------------------------- @@ -53,6 +56,5 @@ html_static_path = ['_static'] html_logo = "_static/pysamoo.png" html_theme_options = { - 'logo_only': True, - 'display_version': False, + 'collapse_navigation': False, } diff --git a/docs/source/index.ipynb b/docs/source/index.ipynb index 6ce2144..74aed41 100644 --- a/docs/source/index.ipynb +++ b/docs/source/index.ipynb @@ -58,7 +58,7 @@ ".. admonition:: Overview\n", " :class: myOwnStyle\n", "\n", - " - `License <#License>`_: GNU Affero General Public License (AGPL).\n", + " - `License <#License>`_: Non-commercial use only (contact us for commercial licensing).\n", " - `Installation <#Installation>`_: How to install the current release of pysamoo.\n", " - `Algorithms <#Algorithms>`_: An overview of algorithms and their underlying concepts.\n", " - `Usage <#Usage>`_: Instructions and code snippets to execute algorithms.\n", @@ -88,7 +88,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "**GNU Affero General Public License (AGPL)**: The GNU Affero General Public License is a modified version of the ordinary GNU GPL version 3. It has one added requirement: if you run a modified program on a server and let other users communicate with it there, your server must also allow them to download the source code corresponding to the modified version running there." + "**Non-commercial License**: pysamoo is distributed under a license that permits **non-commercial use only** (academic, research, and personal use). If you intend to use pysamoo for commercial purposes, please contact us to arrange a commercial license." ] }, { @@ -332,8 +332,7 @@ "source": [ "**Publication**:\n", "\n", - "`Julian Blank and Kalyanmoy Deb. 2021. PSAF: a probabilistic surrogate-assisted framework for single-objective optimization. In Proceedings of the Genetic and Evolutionary Computation Conference (GECCO '21). Association for Computing Machinery, New York, NY, USA, 652–659. `_\n", - "\n" + "`Julian Blank and Kalyanmoy Deb. 2021. PSAF: a probabilistic surrogate-assisted framework for single-objective optimization. In Proceedings of the Genetic and Evolutionary Computation Conference (GECCO '21). Association for Computing Machinery, New York, NY, USA, 652–659. `_\n" ] }, { @@ -356,13 +355,12 @@ " url = {https://doi.org/10.1145/3449639.3459297},\n", " doi = {10.1145/3449639.3459297},\n", " booktitle = {Proceedings of the Genetic and Evolutionary Computation Conference},\n", - " pages = {652–659},\n", + " pages = {652–659},\n", " numpages = {8},\n", " keywords = {simulation optimization, metamodel-based optimization, surrogate-assisted optimization, genetic algorithms, evolutionary computing},\n", " location = {Lille, France},\n", " series = {GECCO '21}\n", - " }\n", - "\n" + " }\n" ] }, { @@ -396,10 +394,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "For single-objective optimization, the $\\alpha$-phase has already been described. There, the comparison of the two solutions is based only on one single objective value. \n", - "For the more generic version with constrainAnalogously to PSAF, GPSAF further increases the surrogate's impact by looking $\\beta$ iterations into the future through calling infill *and* advance of the baseline algorithm repetitively.\n", - "To obtain the $\\beta$-solution for constrained multi-objective problems, we use a so-called Probabilistic Knockout Tournament (PKT) to select solutions from each cluster with the goal of self-adaptively exploiting surrogates. The goal is to use surrogates more when they provide accurate predictions but use them more carefully when they provide only rough estimations. \n", - "Necessary for generalization, PKT also applies to problems with multiple objectives and constraints, often with varying complexities and surrogate errors to be considered.ts and objectives, the winner of each solution pool is determined as follows: if *all* solutions are infeasible, select the least infeasible solution; otherwise, select a non-dominated solution (break ties randomly). For both the constraint and objective values, only ASEs are used. \n", + "For single-objective optimization, the $\\alpha$-phase has already been described. There, the comparison of two solutions is based only on a single objective value.\n", + "For the more generic version with constraints and objectives, the winner of each solution pool is determined as follows: if *all* solutions are infeasible, select the least infeasible solution; otherwise, select a non-dominated solution (breaking ties randomly). For both the constraint and objective values, only ASEs are used.\n", "Otherwise, the $\\alpha$-phase remains the same, including its responsibilities and mechanics." ] }, @@ -408,7 +404,7 @@ "metadata": {}, "source": [ "Analogously to PSAF, GPSAF further increases the surrogate's impact by looking $\\beta$ iterations into the future through calling infill *and* advance of the baseline algorithm repetitively.\n", - "To obtain the $\\beta$-solution for constrained multi-objective problems, we use a so-called PKT to select solutions from each cluster with the goal of self-adaptively exploiting surrogates. The goal is to use surrogates more when they provide accurate predictions but use them more carefully when they provide only rough estimations. \n", + "To obtain the $\\beta$-solutions for constrained multi-objective problems, we use a so-called Probabilistic Knockout Tournament (PKT) to select solutions from each cluster with the goal of self-adaptively exploiting the surrogates. The idea is to rely on the surrogates more when they provide accurate predictions, and more carefully when they provide only rough estimations.\n", "Necessary for generalization, PKT also applies to problems with multiple objectives and constraints, often with varying complexities and surrogate errors to be considered." ] }, @@ -494,7 +490,14 @@ { "cell_type": "code", "execution_count": 1, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-05T19:32:22.917768Z", + "iopub.status.busy": "2026-07-05T19:32:22.917616Z", + "iopub.status.idle": "2026-07-05T19:32:34.867263Z", + "shell.execute_reply": "2026-07-05T19:32:34.866618Z" + } + }, "outputs": [ { "name": "stdout", @@ -503,28 +506,118 @@ "==========================================================================\n", "n_gen | n_eval | n_nds | igd | gd | hv \n", "==========================================================================\n", - " 1 | 50 | 8 | 1.5264428232 | 2.2537834613 | 0.000000E+00\n", - " 2 | 60 | 12 | 0.0447088272 | 0.5062280434 | 0.6033429744\n", - " 3 | 70 | 20 | 0.0317166787 | 0.1679151161 | 0.6185749342\n", - " 4 | 80 | 24 | 0.0254483220 | 0.0084090160 | 0.6309981786\n", - " 5 | 90 | 29 | 0.0230370295 | 0.0144214557 | 0.6344052194\n", - " 6 | 100 | 35 | 0.0212245222 | 0.0132708565 | 0.6370941415\n", - " 7 | 110 | 42 | 0.0195966238 | 0.0130647742 | 0.6396760863\n", - " 8 | 120 | 48 | 0.0177374193 | 0.0108200540 | 0.6420831108\n", - " 9 | 130 | 50 | 0.0175224609 | 0.0174397893 | 0.6422432439\n", - " 10 | 140 | 53 | 0.0158996130 | 0.0094427526 | 0.6444796025\n", - " 11 | 150 | 59 | 0.0153706224 | 0.0176365671 | 0.6454315717\n", - " 12 | 160 | 63 | 0.0138676188 | 0.0168468611 | 0.6472923167\n", - " 13 | 170 | 69 | 0.0136776440 | 0.0160061467 | 0.6478761757\n", - " 14 | 180 | 74 | 0.0125680488 | 0.0153520648 | 0.6495825568\n", - " 15 | 190 | 79 | 0.0121553028 | 0.0148969083 | 0.6500744968\n", - " 16 | 200 | 83 | 0.0114381989 | 0.0104171897 | 0.6513000952\n" + " 1 | 50 | 7 | 1.7675711524 | 2.3765929911 | 0.000000E+00\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 2 | 60 | 6 | 1.7675711524 | 2.1162074488 | 0.000000E+00\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 3 | 70 | 5 | 0.4178860330 | 1.1534020510 | 0.2671478644\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 4 | 80 | 5 | 0.4178860330 | 1.1534020510 | 0.2671478644\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 5 | 90 | 6 | 0.4178860330 | 1.7837506347 | 0.2671478644\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 6 | 100 | 6 | 0.4178860330 | 1.7799451332 | 0.2671478644\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 7 | 110 | 7 | 0.4178860330 | 1.8381888849 | 0.2671478644\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 8 | 120 | 7 | 0.4178860330 | 1.8381888849 | 0.2671478644\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 9 | 130 | 9 | 0.1262129231 | 0.9813825130 | 0.4978804670\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 10 | 140 | 14 | 0.1045699641 | 0.7141841390 | 0.5620521985\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 11 | 150 | 17 | 0.0460449706 | 0.3370675354 | 0.5989570710\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 12 | 160 | 21 | 0.0423479522 | 0.2886976229 | 0.6058987383\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 13 | 170 | 28 | 0.0263846443 | 0.3061263831 | 0.6264093373\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 14 | 180 | 31 | 0.0214252115 | 0.2562413670 | 0.6326874041\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 15 | 190 | 33 | 0.0191260510 | 0.2409429082 | 0.6375164145\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 16 | 200 | 34 | 0.0190653384 | 0.2340198567 | 0.6376240312\n" ] }, { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 1, @@ -533,7 +626,7 @@ }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAArYAAAIQCAYAAAB0Ri0fAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAABxGklEQVR4nO3dd1iV9f/H8edhCjJyTxyoaW5zoYIDcGuWZtkwrSzLhpplplnZUMummWVZWuZILUeO3FtAxD3T3HsDgoDA/f3j85UitTCFczi+HtfFhec+9znnfX/Pz1+v6+P7fn9slmVZiIiIiIjkci72LkBERERE5FZQsBURERERp6BgKyIiIiJOQcFWRERERJyCgq2IiIiIOAUFWxERERFxCgq2IiIiIuIU3OxdgL2lp6dz7NgxfH19sdls9i5HRERERP7Gsizi4+MpXrw4Li7XX5e97YPtsWPHCAgIsHcZIiIiIvIvDh8+TMmSJa/7/G0fbH19fQHzP5Sfn5+dqxERERGRv4uLiyMgICAjt13PbR9sr7Qf+Pn5KdiKiIiIOLB/axvVzWMiIiIi4hQUbEVERETEKSjYioiIiIhTULAVEREREaegYCsiIiIiTkHBVkREREScgoKtiIiIiDgFBVsRERERcQoKtiIiIiLiFBRsRURERMQpKNiKiIiIiFNQsBURERERp6BgKyIiIiJOwc3eBdw2Ll2Cn3+GHTvAxwc6doRKlexdlYiIiIjT0IptTpg9G0qWhK5dYdIkeP99uOsueOABSEiwd3UiIiIiTkHBNrutXg2dOkHjxrB3Lxw4AKdOwbhxMG+eCbsiIiIictPUipDd3n0XqlWDadPA7f//c3t6Qvfu4O4Ojz4KmzZBzZp2LFJEREQk99OKbXa6cAEWLIBnn/0z1P7Vgw9CoUIwdWqOlyYiIiLibBRss1N8vPldsuS1n3dzg2LFIC4u52oSERERcVIKttmpcGHw8zN9ttdy6hTs2gUVKuRsXSIiIiJOSME2O3l6Qrdu8OWX8McfmZ+zLHjjDXBxMX22IiIiInJTdPNYdnvjDdNnGxQEL74IoaFw4gR89RUsXgxjxkCBAvauUkRERCTX04ptditY0LQidOwIw4dDcDDcf7+5sWzmTHj6aXtXKCIiIuIUbJZlWfYuwp7i4uLw9/cnNjYWPz+/7P2wixfh4EHw9YVSpbL3s0REREScRFbzmloRcpKPD1SpYu8qRERERJySWhFERERExCko2IqIiIiIU1CwFRERERGnoGArIiIiIk5BwVZEREREnIKCrYiIiIg4BQVbEREREXEKDhdsU1JSGDBgAG5ubhw4cOAfz128eDH33HMPoaGhBAUF0apVK7Zs2ZIzhYqIiIiIQ3GoYHvgwAGaNGnC8ePHSUtL+9fzn3nmGdq3b8/SpUuJjIykTp06hIeHc/r06RyoVkREREQciUMF24sXLzJhwgQef/zxLJ1fp04dnnzyyYzHffr04fTp0yxevDi7ShQRERERB+VQW+pWrVoVgCNHjmTp/ClTpmR6nCdPHgCSk5NvbWEiIiIi4vAcKtjerIiICLy8vGjXrt11z0lOTs4UfOPi4nKiNBERERHJZg7VinAzLMvi3Xff5b333qNgwYLXPW/YsGH4+/tn/AQEBORglSIiIiKSXZwm2L711luUKlWKvn37/uN5r732GrGxsRk/hw8fzqEKRURERCQ7OUUrwldffcXGjRv55Zdf/vVcT09PPD09c6AqEREREclJuX7FdvLkyUyfPp2pU6fi5ubGvn37NBVBRERE5DaUq1ZsH374YVxdXZkwYQIAc+bMYcCAAYwfP55t27YBEBMTw/HjxwkPD7dnqSIiIiKSwxwq2KakpNCiRQsuXLgAQJcuXQgICGDatGkAJCUl4eLy5yLz448/zpkzZwgNDc30Pm+++WaO1SwiIiIijsFmWZZl7yLsKS4uDn9/f2JjY/Hz87N3OSIiIiLyN1nNa7m+x1ZEREREBBRsRURERMRJKNiKiIiIiFNQsBURERERp6BgKyIiIiJOQcFWRERERJyCgq2IiIiIOAUFWxERERFxCgq2IiIiIuIUFGxFRERExCko2IqIiIiIU1CwFRERERGnoGArIiIiIk5BwVZEREREnIKCrYiIiIg4BQVbEREREXEKCrYiIiIi4hQUbEVERETEKSjYioiIiIhTULAVEREREaegYCsiIiIiTkHBVkREREScgoKtiIiIiDgFBVsRERERcQoKtiIiIiLiFBRsRURERMQpKNiKiIiIiFNQsBURERERp6BgKyIiIiJOQcFWRERERJyCgq2IiIiIOAUFWxERERFxCgq2IiIiIuIUFGxFRERExCko2IqIiIiIU1CwFRERERGnoGArIiIiIk5BwVZEREREnIKCrYiI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vPOO2Uziq6/scYUi2UbBVkRExBFVrGjGepUr98/nPfYYhaZMoWPDhnxWrBhfurrS5dFHKX7uHCl+fqyJjWXY6dN07dqVTw4dIgZITUw0bQqzZuXIpYjkFM3/EBERyc1efhl++gkaN4aAAEqmpfFI3rw8bLPxx4ULrKxdm1UVKnDmzBmWrl3LUsD3229pFBtL4wsXqJKejouL1rnEOajHVj22IiKS2+3dC088YXY8u8JmA39/OHECy8ODHd98w6qePVnt709sSAisWAF585K/RQuCg4MJCQmhYsWK2Gw2+12HyHXo5rEsUrAVERGnsW0bvPqqGRfm4WFuFGvd2mzlGxUF7u6kbdzI1gkTWDl8OGubNiXBxyfj5YULFyY4OJjGjRsTGBh47ZB74gR8/TX8+quZslCzJjz77FU9wCK3koJtFinYioiI0/n8cxgyBM6e/fNYQAA895y5EW3ePOjdm9QRI9i4aRMrV64kMjKSpKSkjNOLFStGSEgIISEhlC5d2oTcyEho08YE2vvugzvuMLud7dsHb7xhPjMpyWwa4e9vfkRuAQXbLFKwFRERp7VvH/z4o7lJbMMGc6x2bejTBx55xLQr/F9KSgrr169n1apVrFu3jpSUlIznAgICCKlbl5B+/ShZuTLMng3585sn09Nh+HAYNAhatoTVqyEhwbx3mzYweLCZyytyExRss0jBVkREbguXLpldyLKw41hSUhLr1q1j5cqVxMTEkJqaCgcPwtatlOnWjZA2bQgODqb4lRm4x46ZHdPS080M3gYN4MABGD3abO/766/QokX2Xp84NQXbLFKwFRERub6EhASioqJY1bs3G48dI61hw4znAgMDCQkJIXjSJIouXQqxsWY+rqurOSElBTp0MLunHTgA7u72uQjJ9RRss0jBVkREJAu6dCH+0CEihwxh9erVbNq0yWzpm5ICixZRoXx5Gu3ZQ/DRoxT5625mW7ZAjRrw88/QsaP96pdcLat5TXNsRURE5N+FhODbuzfN77qL5s2bExcXR0REBKt++oktlsWeU6fYky8f43v2pEKFCgQHBxMcHEzh6tXNVr47d2ZPXbGxJlwXKACax3vb04qtVmxFRET+XVwclCkDtWrBjBlw5b+ZO3YQW6UKEcCq++5ja2oqf40WFUqXJnjMGILffJPCr79+6+qZORNGjIC1a83jMmWgVy9z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T0/PTMc9PT0znruZ868YNmwYQ4YMudlyRURERJyWn58foaGhhIaGkpqaetWUhU2bNtGwYUN7l3kVhwm23t7eAJlWU688vvLczZx/xWuvvcZLL72U8TguLo6AgID/XLeIiIiIM3Nzc6N69epUr16dHj16cPToUaKjo6lfv769S7uKwwTbAgUK4O/vz8mTJzMdP3HiBIGBgTd9/hWenp5XrfKKiIiISNaUKFGCEiVK2LuMa3KxdwF/FRoaSkxMTMZjy7LYsGED4eHht+R8EREREXFeDhVsBwwYwNy5c9m7dy8AEydOxNXVlW7dugEQHBzMoEGDsny+iIiIiNw+HKYVAaBevXqMHz+eLl264OXlhYuLCwsWLMjYbCExMTFTT+2/nS8iIiIitw+HmmNrD5pjKyIiIuLYsprXHKoVQURERETkv1KwFRERERGnoGArIiIiIk5BwVZEREREnIKCrYiIiIg4BQVbEREREXEKCrYiIiIi4hQUbEVERETEKSjYioiIiIhTULAVEREREaegYCsiIiIiTkHBVkREREScgoKtiIiIiDgFN3sXYG+WZQEQFxdn50pERERE5Fqu5LQrue16bvtgGx8fD0BAQICdKxERERGRfxIfH4+/v/91n7dZ/xZ9nVx6ejrHjh3D19cXm82W7Z8XFxdHQEAAhw8fxs/PL9s/T24tfX+5n77D3E/fYe6n7zB3s8f3Z1kW8fHxFC9eHBeX63fS3vYrti4uLpQsWTLHP9fPz09/mXMxfX+5n77D3E/fYe6n7zB3y+nv759Waq/QzWMiIiIi4hQUbEVERETEKSjY5jBPT0/efPNNPD097V2K/Af6/nI/fYe5n77D3E/fYe7myN/fbX/zmIiIiIg4B63YioiIiIhTULAVEREREaegYCsiIiIiTkHBVkREREScgoJtNpgxYwZ169YlJCSEJk2asH379lt6vmSvG/k+pk6dSosWLQgLC6Nu3bp07tyZAwcO5Fyxck3/9e/UqFGjsNlsLF++PHsLlH91o9/hvn376NSpE82aNaNKlSoEBQWxfv36HKpWruVGvsPk5GT69u1LjRo1aNKkCfXr12fGjBk5WK38XUpKCgMGDMDNzS1L/11bvXo1QUFBNGnShKCgIFatWpX9RV6LJbdUVFSU5evra/3++++WZVnW999/b5UoUcKKi4u7JedL9rrR78Pd3d36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", + "image/png": "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Y0e1b99e48ePL/HxkkZMo6KilJ6ertDQ0IoqEwAAAGXkdrvlcrlOmdds7WP67bffnnDM399fR44cKfUcf39/hYaGFrsBAACg8rM1mA4dOlR79+713E9KStKGDRvUqVMnG6sCAACAHWydY9qsWTNdffXVuu++++Tl5aXx48erVatWGjZsmJ1lAQAAwAa2jpguW7ZM77//vjIyMpSSkqLnn39ev/76q/z9/e0sCwAAADZw3OKn01XWybQAAACwR6VY/AQAAAAUIZgCAADAEQimAAAAcASCKQAAAByBYAoAAABHIJgCAADAEQimAAAAcASCKQAAAByBYAoAAABHIJgCAADAEQimAAAAcASCKQAAAByBYAoAAABHIJgCAADAEQimAAAAcASCKQAAAByBYAoAAABHIJgCAADAEQimAAAAcASCKQAAAByBYAoAAABHIJgCAADAEQimAAAAcASCKQAAAByBYAoAAABHIJgCAADAEQimAAAAcASCKQAAAByBYAoAAABHIJgCAADAEQimAAAAcASCKQAAAByBYAoAAABHIJgCAADAEQimAAAAcASCKQAAAByBYAoAAABHIJgCAADAEQimAAAAcASCKQAAAByBYAoAAABHIJgCAADAEQimAAAAcASCKQAAAByBYAoAAABHIJgCAADAEQimAAAAcASCKQAAAByBYAoAAABHIJgCAADAEQimAAAAcASCKQAAAByBYAoAAABHIJgCAADAEQimAAAAcASCKQAAAByBYAoAAABHIJgCAADAEQimAAAAcATHBNN9+/bpq6++UmZmpt2lAAAAwAaOCKb5+fkaMGCABg4cqMTERLvLAQAAgA0cEUyfe+45de7c2e4yAAAAYCPbg+nSpUuVlJSkgQMH2l0KAAAAbORj55unp6frlVde0fTp07Vx48YynZOTk6OcnBzPfbfbXV7lAQAAoALZOmL66KOP6vXXX1dQUFCZzxkzZoxcLpfnFhUVVY4VAgAAoKLYFkynTJmipKQkrV+/Xl999ZXmzZsnSfruu+/01VdfKTs7u8TzRo0apfT0dM8tNja2IssGAABAObHtUv7Bgwflcrk0d+5cSVJycrIkM+c0ODhY/fr1U0BAwAnn+fv7y9/fv0JrBQAAQPnzsizLsrsISfrtt9906aWXaseOHWratGmZz3O73XK5XEpPT1doaGg5VggAAIAzUda8ZvuqfMlcvj/2Un7RzwAAAPjrcEQwXbp0qXbs2KE77rhD69at06+//mp3SQAAAKhgtraLKvLmm2/aXQIAAABs5ogRUwAAAIBgCgAAAEcgmAIAAMARCKYAAABwBIIpAAAAHIFgCgAAAEcgmAIAAMARCKYAAABwBIIpAAAAHIFgCgAAAEcgmAIAAMARCKYAAABwBIIpAAAAHIFgCgAAAEcgmAIAAMARCKYAAABwBIIpAAAAHIFgCgAAAEcgmAIAAMARCKYAAABwBIIpAAAAHIFgCgAAAEcgmAIAAMARCKYAAABwBIIpAAAAHMHH7gIqjexs6dtvpfXrpapVpf79pYsusrsqAACA8wYjpmUxf77UoIF0xx3Sf/8rjRsndewo9e4tpabaXR0AAMB5gWB6KtHRZnS0Qwdp61Zp924pMdEE1FWrpBtvlCzL7ioBAAAqPS7ln8qrr0pRUdJ330n+/uaYj480YIAUHCxdd520eLF01VW2lgkAAFDZMWJ6Mvn5Zl7pffcdDaXHuvZaqWlTadq0iq8NAADgPEMwPZnsbCkvT4qIKPlxLy/zmNtdsXUBAACchwimJxMUJNWrJ/3yS8mPZ2RIv/8uNW9esXUBAACchwimJ+PlZS7jT5pkFkEdy7Kk0aOlrCzp3nttKQ8AAOB8wuKnU3nsMWnWLKl7d+nBB02LqEOHpM8/l378UXrrLbM4CgAAAGeFYHoqwcHSzz9Lzz0nffyx9Npr5nj79tLUqdKtt9paHgAAwPnCy7IqdxNOt9stl8ul9PR0hYaGlu+bHTkixcZKAQFmlNTLq3zfDwAA4DxQ1rzGiOnpqFqVhU4AAADlhMVPAAAAcASCKQAAAByBYAoAAABHIJgCAADAEQimAAAAcASCKQAAAByBYAoAAABHIJgCAADAEQimAAAAcASCKQAAAByBYAoAAABHIJgCAADAEQimAAAAcASCKQAAAByBYAoAAABHIJgCAADAEQimAAAAcASCKQAAAByBYAoAAABHIJgCAADAEQimAAAAcASCKQAAABzBx+4CcnNztXLlSmVmZqp58+Zq0qSJ3SUBAADABraOmH7xxRdq1KiRRo4cqXfeeUetW7fWP/7xDxUUFNhZFgAAAGxgazCdOHGinnjiCS1fvlw//fST5s6dqylTpmjZsmV2lgUAAAAb2BpMr7jiCt19992e+/Xq1ZMkBQYG2lUSAAAAbHLWc0ynT5+u//73v+rZs6eGDBkiSZo7d66io6M1cuTIk577wgsvSJIWLlyoTZs2afLkyXrxxRfVuXPnUs/JyclRTk6O577b7T7bjwAAAAAHOKsR0z179mjIkCG69tpr9fvvv+vhhx+WJCUmJmrr1q1lfp2FCxdq5syZ8vLyUkRExEmfO2bMGLlcLs8tKirqbD4CAAAAHMLLsizrTE/+73//q6lTp2r69OmSpJdeekm5ublq3LixFi9erC+//PK0Xm/z5s26+OKL9cEHH2jQoEElPqekEdOoqCilp6crNDT0TD8KAAAAyonb7ZbL5TplXivTiGlaWlqxMFikVatW2rZtm+f+008/rQMHDuiTTz455WsWFBRoyZIlxY61bt1aXbp00YwZM0o9z9/fX6GhocVuAAAAqPzKFEw///xzPfXUU5KkhIQEHTp0SJIJkr1799bcuXM9z/3ggw/UsGFD1ahR46SveeTIEd1yyy06fPiw51heXp5iYmJUrVq10/4gAAAAqNzKtPgpNDRUu3btkiRNnjxZiYmJevPNNyVJb7zxRrHnVqlSRVOmTDn1G/v4yNvbW9dcc40GDRokHx8fTZ48WUlJSXrkkUdO93MAAACgkitTMO3atatGjBihZs2aadeuXQoKCjrrNw4ICNCePXs0efJk/f7778rOzlaPHj301VdfedpGAQAA4K+jzIufvv76a7322mvasGGDqlSpoubNm6tdu3Zq37695886deqUd70nKOtkWgAAANijrHnttFflv/7661q5cqWuvPJKRUdHa/369dq8ebOys7NVq1YtPfvssxo2bNhZf4CyIpgCAAA4W1nz2mk32L/55pvVvXt3XXLJJZ5j+fn52rZtm6Kjo1WrVq0zqxgAAAB/aWfVx9QJGDEFAABwtnPaxxQAAAAobwRTAAAAOALBFAAAAI5AMAUAAIAjEEwBAADgCARTAAAAOALBFAAAAI5AMAUAAIAjEEwBAADgCARTAAAAOALBFAAAAI5AMAUAAIAjEEwBAADgCARTAAAAOALBFAAAAI5AMAUAAIAjEEwBAADgCARTAAAAOALBFAAAAI5AMAUAAIAjEEwBAADgCARTAAAAOALBFAAAAI5AMAUAAIAjEEwBAADgCARTAAAAOALBFAAAAI5AMAUAAIAjEEwBAADgCARTAAAAOALBFAAA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bqwHH3xQ7du3t+kTYNasWZowYYIyMzN1zTXX6JFHHik21/CGG25QZGSkxo0bJ8n8d/vhhx9q1apVOnLkiJo1a6YHH3xQLVu2tOsj/KW98847mjFjRrFjzz77rHr16iXJ7FY5cOBADR48WIMGDZJkRlHff/99zZs3TwEBAbrzzjs1YMCAii5dBFMAAAA4AnNMAQAA4AgEUwAAADgCwRQAAACOQDAFAACAIxBMAQAA4AgEUwAAADgCwRQAAACOQDAFAAcoKCjQa6+9pl69eqlz587auHGj3SUBQIXzsbsAAID03nvv6bPPPtNHH32k8PBwtWjRwu6SAKDCsfMTADhAx44ddfvtt+uxxx6zuxQAsA2X8gHARuvXr1enTp20fv16ffTRR+rUqZOWLVtmd1kAYAtGTAHARmlpafruu+80aNAgLVmyRIGBgWrVqpUCAwPtLg0AKhxzTAHARmFhYSooKFBUVJS6d+9udzkAYCsu5QOAzdavX6/27dt77v/444/q27ev+vbtq+XLl9tXGABUMIIpANgsOjpa7dq189xv3bq1hg4dqqysLCUkJNhYGQBULIIpANhsw4YNxUZM69evr759+yoiIsK+ogDABgRTALDR3r17lZaWVmzEFAD+qgimAGCj6OhoBQcHq0mTJnaXAgC2I5gCgI26du2q3377TV5eXnaXAgC2o48pADjM+vXr9dRTTyk6Olp16tRR06ZN9c0339hdFgCUO/qYAoDDREREaOjQoZ77vr6+NlYDABWHEVMAAAA4AnNMAQAA4AgEUwAAADgCwRQAAACOQDAFAACAIxBMAQAA4AgEUwAAADgCwRQAAACOQDAFAACAIxBMAQAA4AgEUwAAADjC/wNX46coh/dZJQAAAABJRU5ErkJggg==", "text/plain": [ "
" ] @@ -553,7 +646,7 @@ "algorithm = SSANSGA2(n_initial_doe=50,\n", " n_infills=10,\n", " surr_pop_size=100,\n", - " surr_n_gen=50)\n", + " surr_n_gen=20)\n", "\n", "res = minimize(\n", " problem,\n", @@ -578,47 +671,23 @@ { "cell_type": "code", "execution_count": 2, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-05T19:32:34.872282Z", + "iopub.status.busy": "2026-07-05T19:32:34.872026Z", + "iopub.status.idle": "2026-07-05T19:33:00.472481Z", + "shell.execute_reply": "2026-07-05T19:33:00.471931Z" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "================================================================================================================================================================\n", - "n_gen | n_eval | f_min | f_gap | r2 | bias | mae | model \n", - "================================================================================================================================================================\n", - " 1 | 30 | 2.036794E+01 | 2.036794E+01 | - | - | 0.2738756231 | RBF[kernel=linear,tail=constant,normalized=True]\n", - " 2 | 40 | 1.614481E+01 | 1.614481E+01 | 0.3763933967 | 0.7000000000 | 0.5348634440 | RBF[kernel=cubic,tail=linear+quadratic,normalized=False]\n", - " 3 | 50 | 1.614481E+01 | 1.614481E+01 | 0.3976668792 | 0.7000000000 | 0.6351592863 | RBF[kernel=linear,tail=constant,normalized=True]\n", - " 4 | 60 | 1.479848E+01 | 1.479848E+01 | 0.2795887538 | 0.7000000000 | 0.8588945004 | RBF[kernel=mq,tail=linear+quadratic,normalized=False]\n", - " 5 | 70 | 1.310293E+01 | 1.310293E+01 | -2.495997E-01 | 0.7000000000 | 1.0450900866 | RBF[kernel=linear,tail=linear+quadratic,normalized=False]\n", - " 6 | 80 | 1.268252E+01 | 1.268252E+01 | 0.1339056137 | 0.7000000000 | 1.4391106360 | RBF[kernel=linear,tail=constant,normalized=True]\n", - " 7 | 90 | 1.198853E+01 | 1.198853E+01 | 0.1004963738 | 0.7000000000 | 1.1109921715 | krg-cont\n", - " 8 | 100 | 1.070961E+01 | 1.070961E+01 | -1.356016E-01 | 0.7000000000 | 1.1038348109 | krg-cont\n", - " 9 | 110 | 9.0245380210 | 9.0245380210 | -3.871513E-02 | 0.7000000000 | 1.1543317740 | krg-cont\n", - " 10 | 120 | 8.8940807360 | 8.8940807360 | -3.268868E-02 | 0.7000000000 | 0.8929508896 | krg-cont\n", - " 11 | 130 | 8.4131436015 | 8.4131436015 | 0.1006854678 | 0.7000000000 | 0.9085243047 | krg-cont\n", - " 12 | 140 | 7.8246574061 | 7.8246574061 | -8.313746E-01 | 0.7000000000 | 0.5755873561 | krg-lin\n", - " 13 | 150 | 7.0834789444 | 7.0834789444 | 0.4176491944 | 0.7000000000 | 0.6266697638 | krg-cont\n", - " 14 | 160 | 6.0646712483 | 6.0646712483 | 0.2139484322 | 0.7000000000 | 0.4735844314 | krg-lin\n", - " 15 | 170 | 5.9366538601 | 5.9366538601 | 0.4389178038 | 0.7000000000 | 0.5479664682 | krg-lin\n", - " 16 | 180 | 5.6660865834 | 5.6660865834 | 0.0852439643 | 0.7000000000 | 0.5417697700 | krg-cont\n", - " 17 | 190 | 5.0408470264 | 5.0408470264 | -1.175129E-01 | 0.7000000000 | 0.5883136752 | krg-cont\n", - " 18 | 200 | 4.6010645765 | 4.6010645765 | -6.175937E-01 | 0.7000000000 | 0.5818244380 | krg-cont\n", - " 19 | 210 | 4.5254801853 | 4.5254801853 | -1.176425E+00 | 0.7000000000 | 0.6073626579 | krg-cont\n", - " 20 | 220 | 3.9720804105 | 3.9720804105 | -2.661123E+00 | 0.7000000000 | 0.5095595451 | krg-cont\n", - " 21 | 230 | 3.9720804105 | 3.9720804105 | -2.542042E+00 | 0.7000000000 | 0.5437289305 | krg-cont\n", - " 22 | 240 | 3.6608112233 | 3.6608112233 | -2.833233E+00 | 0.7000000000 | 0.4527138832 | krg-cont\n", - " 23 | 250 | 3.4490801784 | 3.4490801784 | -2.355759E+00 | 0.7000000000 | 0.4256093867 | krg-lin\n", - " 24 | 260 | 3.0871007367 | 3.0871007367 | -4.493002E+00 | 0.7000000000 | 0.5288772233 | krg-lin\n", - " 25 | 270 | 3.0871007367 | 3.0871007367 | -5.774866E+00 | 0.7000000000 | 0.6508483015 | krg-lin\n", - " 26 | 280 | 3.0020921309 | 3.0020921309 | -9.136042E+00 | 0.7000000000 | 0.5603836037 | krg-lin\n", - " 27 | 290 | 2.9161694816 | 2.9161694816 | -8.368297E+00 | 0.7000000000 | 0.6311346722 | krg-lin\n", - " 28 | 300 | 2.8268436055 | 2.8268436055 | -1.130324E+01 | 0.7000000000 | 0.6046665921 | krg-lin\n", "Best solution found: \n", - "X = [ 0.97346909 0.03114564 -1.06840523 0.04009389 -0.0521602 0.27861872\n", - " 1.03606787 -0.07359098 -0.98842002 -0.03037685]\n", - "F = [2.82684361]\n", + "X = [ 0.01542188 0.80274683 -1.09073293 0.06880787 -0.14998365 -0.15102526\n", + " 0.052121 0.0047629 0.09987345 -0.84853149]\n", + "F = [2.52998523]\n", "CV=[0.]\n" ] } @@ -655,47 +724,23 @@ { "cell_type": "code", "execution_count": 3, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-05T19:33:00.474200Z", + "iopub.status.busy": "2026-07-05T19:33:00.474058Z", + "iopub.status.idle": "2026-07-05T19:33:28.199865Z", + "shell.execute_reply": "2026-07-05T19:33:28.199474Z" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "================================================================================================================================================================\n", - "n_gen | n_eval | f_min | f_gap | r2 | bias | mae | model \n", - "================================================================================================================================================================\n", - " 1 | 30 | 2.036794E+01 | 2.036794E+01 | - | - | 0.2738756231 | RBF[kernel=linear,tail=constant,normalized=True]\n", - " 2 | 40 | 1.647220E+01 | 1.647220E+01 | 0.3763933967 | 0.7000000000 | 0.6182901894 | RBF[kernel=cubic,tail=linear+quadratic,normalized=False]\n", - " 3 | 50 | 1.647220E+01 | 1.647220E+01 | 0.3341034259 | 0.7000000000 | 1.0337146201 | RBF[kernel=linear,tail=linear,normalized=True]\n", - " 4 | 60 | 1.647220E+01 | 1.647220E+01 | -2.325472E-01 | 0.7000000000 | 1.1952242311 | RBF[kernel=linear,tail=linear,normalized=True]\n", - " 5 | 70 | 1.526461E+01 | 1.526461E+01 | -2.483244E-01 | 0.7000000000 | 1.3548503486 | RBF[kernel=linear,tail=linear,normalized=True]\n", - " 6 | 80 | 1.353941E+01 | 1.353941E+01 | -2.854016E-01 | 0.7000000000 | 1.7725365031 | RBF[kernel=mq,tail=constant,normalized=False]\n", - " 7 | 90 | 1.353941E+01 | 1.353941E+01 | -6.876366E-01 | 0.7000000000 | 0.4617090285 | krg-cont\n", - " 8 | 100 | 1.013393E+01 | 1.013393E+01 | 0.9168512037 | 0.9168512037 | 0.5703039705 | krg-cont\n", - " 9 | 110 | 1.013393E+01 | 1.013393E+01 | 0.8668379853 | 0.8668379853 | 0.5693628726 | krg-lin\n", - " 10 | 120 | 9.7623502147 | 9.7623502147 | 0.8575626049 | 0.8575626049 | 0.5840720180 | krg-lin\n", - " 11 | 130 | 9.0558961856 | 9.0558961856 | 0.8603199816 | 0.8603199816 | 0.5523672725 | krg-lin\n", - " 12 | 140 | 9.0558961856 | 9.0558961856 | 0.8717525585 | 0.8717525585 | 0.5662209881 | krg-cont\n", - " 13 | 150 | 9.0237677371 | 9.0237677371 | 0.7948496762 | 0.7948496762 | 0.4791068868 | krg-cont\n", - " 14 | 160 | 6.6233913732 | 6.6233913732 | 0.7726556864 | 0.7726556864 | 0.4877162796 | krg-cont\n", - " 15 | 170 | 6.6233913732 | 6.6233913732 | 0.8429714787 | 0.8429714787 | 0.4170345533 | krg-cont\n", - " 16 | 180 | 5.7617523828 | 5.7617523828 | 0.8788722441 | 0.8788722441 | 0.4330862204 | krg-cont\n", - " 17 | 190 | 5.7617523828 | 5.7617523828 | 0.8581799389 | 0.8581799389 | 0.4180912931 | krg-lin\n", - " 18 | 200 | 3.9941402749 | 3.9941402749 | 0.8439598319 | 0.8439598319 | 0.3781019454 | krg-lin\n", - " 19 | 210 | 3.9941402749 | 3.9941402749 | 0.9041705189 | 0.9041705189 | 0.4175998236 | krg-lin\n", - " 20 | 220 | 3.9914565327 | 3.9914565327 | 0.8239454481 | 0.8239454481 | 0.4864284914 | krg-cont\n", - " 21 | 230 | 3.9914565327 | 3.9914565327 | 0.8487160611 | 0.8487160611 | 0.4507227421 | krg-cont\n", - " 22 | 240 | 3.8211595495 | 3.8211595495 | 0.8687656158 | 0.8687656158 | 0.5130797895 | krg-cont\n", - " 23 | 250 | 3.6463534058 | 3.6463534058 | 0.8626187728 | 0.8626187728 | 0.5621053352 | krg-cont\n", - " 24 | 260 | 3.6463534058 | 3.6463534058 | 0.8201204070 | 0.8201204070 | 0.5304724523 | krg-cont\n", - " 25 | 270 | 3.6463534058 | 3.6463534058 | 0.8196879465 | 0.8196879465 | 0.5066856628 | krg-cont\n", - " 26 | 280 | 3.6463534058 | 3.6463534058 | 0.7784551445 | 0.7784551445 | 0.5778498499 | krg-cont\n", - " 27 | 290 | 3.6463534058 | 3.6463534058 | 0.7348469218 | 0.7348469218 | 0.5360693341 | krg-lin\n", - " 28 | 300 | 3.6463534058 | 3.6463534058 | 0.7136844062 | 0.7136844062 | 0.5894129213 | krg-lin\n", "Best solution found: \n", - "X = [-0.017365 -0.13750357 -0.05619225 0.369787 -0.54064667 0.02531361\n", - " 0.18193453 1.34091596 -0.55839746 -0.31890217]\n", - "F = [3.64635341]\n", + "X = [ 0.49428562 0.83228995 -0.75769707 0.44138409 1.18453923 0.5343856\n", + " 0.53529047 -2.15945008 0.54392258 -0.44272928]\n", + "F = [5.49562287]\n", "CV=[0.]\n" ] } @@ -732,47 +777,23 @@ { "cell_type": "code", "execution_count": 4, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-05T19:33:28.220008Z", + "iopub.status.busy": "2026-07-05T19:33:28.218435Z", + "iopub.status.idle": "2026-07-05T19:33:54.581005Z", + "shell.execute_reply": "2026-07-05T19:33:54.578671Z" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "================================================================================================================================================================\n", - "n_gen | n_eval | f_min | f_gap | r2 | bias | mae | model \n", - "================================================================================================================================================================\n", - " 1 | 30 | 2.036794E+01 | 2.036794E+01 | - | - | 0.2738756231 | RBF[kernel=linear,tail=constant,normalized=True]\n", - " 2 | 40 | 1.332973E+01 | 1.332973E+01 | 0.3763933967 | 0.7000000000 | 0.7291380548 | RBF[kernel=cubic,tail=linear+quadratic,normalized=False]\n", - " 3 | 50 | 1.332973E+01 | 1.332973E+01 | 0.2592841419 | 0.7000000000 | 0.9917695888 | RBF[kernel=mq,tail=linear+quadratic,normalized=False]\n", - " 4 | 60 | 1.332973E+01 | 1.332973E+01 | 0.1644667959 | 0.7000000000 | 1.1552330246 | RBF[kernel=linear,tail=linear,normalized=True]\n", - " 5 | 70 | 1.332973E+01 | 1.332973E+01 | 0.3694414291 | 0.7000000000 | 1.5138747301 | RBF[kernel=linear,tail=linear,normalized=True]\n", - " 6 | 80 | 1.332973E+01 | 1.332973E+01 | 0.2600993833 | 0.7000000000 | 1.8085470200 | RBF[kernel=linear,tail=linear,normalized=True]\n", - " 7 | 90 | 1.241845E+01 | 1.241845E+01 | 0.2038351057 | 0.7000000000 | 0.6335623668 | krg-cont\n", - " 8 | 100 | 7.3256172613 | 7.3256172613 | 0.8492853728 | 0.8492853728 | 0.8690919422 | krg-cont\n", - " 9 | 110 | 7.3256172613 | 7.3256172613 | 0.7486633888 | 0.7486633888 | 1.0053520252 | krg-cont\n", - " 10 | 120 | 6.9655651500 | 6.9655651500 | 0.6028752305 | 0.7000000000 | 1.1338932661 | krg-cont\n", - " 11 | 130 | 5.0841360021 | 5.0841360021 | 0.6039414826 | 0.7000000000 | 1.1598374455 | krg-cont\n", - " 12 | 140 | 5.0841360021 | 5.0841360021 | 0.6085190250 | 0.7000000000 | 1.2980426541 | krg-cont\n", - " 13 | 150 | 4.7082954697 | 4.7082954697 | 0.6037349339 | 0.7000000000 | 1.0560636000 | krg-cont\n", - " 14 | 160 | 3.9307717509 | 3.9307717509 | 0.6416580369 | 0.7000000000 | 0.8196459781 | krg-cont\n", - " 15 | 170 | 3.9307717509 | 3.9307717509 | 0.7689568545 | 0.7689568545 | 0.6934795741 | krg-cont\n", - " 16 | 180 | 3.6005443620 | 3.6005443620 | 0.7947301965 | 0.7947301965 | 0.6828921049 | krg-cont\n", - " 17 | 190 | 3.5337383218 | 3.5337383218 | 0.7862944244 | 0.7862944244 | 0.5887485674 | krg-lin\n", - " 18 | 200 | 3.5337383218 | 3.5337383218 | 0.8054350086 | 0.8054350086 | 0.8994223086 | krg-lin\n", - " 19 | 210 | 3.5337383218 | 3.5337383218 | 0.2592954046 | 0.7000000000 | 0.9427215928 | krg-lin\n", - " 20 | 220 | 3.5337383218 | 3.5337383218 | 0.0777094174 | 0.7000000000 | 1.0415795870 | krg-lin\n", - " 21 | 230 | 3.5337383218 | 3.5337383218 | -2.692331E-01 | 0.7000000000 | 1.0218187624 | krg-lin\n", - " 22 | 240 | 3.5337383218 | 3.5337383218 | -7.771233E-01 | 0.7000000000 | 1.1893524253 | krg-lin\n", - " 23 | 250 | 2.9110175231 | 2.9110175231 | -1.132129E+00 | 0.7000000000 | 0.8852039354 | krg-cont\n", - " 24 | 260 | 2.6880983494 | 2.6880983494 | -1.270011E+00 | 0.7000000000 | 1.0067140755 | krg-cont\n", - " 25 | 270 | 2.6880983494 | 2.6880983494 | -2.820539E+00 | 0.7000000000 | 1.0239253459 | krg-cont\n", - " 26 | 280 | 2.6880983494 | 2.6880983494 | -3.069151E+00 | 0.7000000000 | 1.0617400742 | krg-lin\n", - " 27 | 290 | 2.6260580244 | 2.6260580244 | -3.561686E+00 | 0.7000000000 | 0.9515962802 | krg-lin\n", - " 28 | 300 | 2.6260580244 | 2.6260580244 | -4.387418E+00 | 0.7000000000 | 1.3332084142 | RBF[kernel=cubic,tail=constant,normalized=True]\n", "Best solution found: \n", - "X = [-0.05580694 -0.09378573 -0.37298884 -0.08638338 0.0515077 -0.41373204\n", - " 0.09691779 1.07067447 0.1263253 -0.20414152]\n", - "F = [2.62605802]\n", + "X = [-0.02831155 0.95573091 0.04952542 -0.1871001 -0.36127044 0.40241819\n", + " -1.11214116 0.08892784 -0.20553759 -0.19863074]\n", + "F = [3.15147175]\n", "CV=[0.]\n" ] } @@ -809,47 +830,23 @@ { "cell_type": "code", "execution_count": 5, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-05T19:33:54.582655Z", + "iopub.status.busy": "2026-07-05T19:33:54.582520Z", + "iopub.status.idle": "2026-07-05T19:34:07.049889Z", + "shell.execute_reply": "2026-07-05T19:34:07.048898Z" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "================================================================================================================================================\n", - "n_gen | n_eval | f_min | f_gap | n_influenced | mae | model \n", - "================================================================================================================================================\n", - " 1 | 30 | 2.036794E+01 | 2.036794E+01 | - | 0.2395537352 | RBF[kernel=gaussian,tail=quadratic,normalized=True]\n", - " 2 | 40 | 9.6180845318 | 9.6180845318 | 4/10 | 0.7292616206 | RBF[kernel=gaussian,tail=quadratic,normalized=False]\n", - " 3 | 50 | 9.5397702619 | 9.5397702619 | 4/10 | 1.2838259268 | RBF[kernel=gaussian,tail=quadratic,normalized=False]\n", - " 4 | 60 | 8.3350419429 | 8.3350419429 | 5/10 | 1.1933475702 | RBF[kernel=gaussian,tail=linear+quadratic,normalized=True]\n", - " 5 | 70 | 8.2371788716 | 8.2371788716 | 4/10 | 1.1919714473 | RBF[kernel=gaussian,tail=linear+quadratic,normalized=True]\n", - " 6 | 80 | 8.0191285291 | 8.0191285291 | 2/10 | 0.8810109187 | kriging-const-ARD\n", - " 7 | 90 | 7.8519168526 | 7.8519168526 | 4/10 | 0.7338034351 | kriging-lin\n", - " 8 | 100 | 7.7553809035 | 7.7553809035 | 2/10 | 0.7521845637 | kriging-lin\n", - " 9 | 110 | 7.3837499736 | 7.3837499736 | 6/10 | 0.8561025281 | kriging-lin\n", - " 10 | 120 | 7.3837499736 | 7.3837499736 | 6/10 | 2.7239305748 | kriging-quadr\n", - " 11 | 130 | 7.3837499736 | 7.3837499736 | 4/10 | 1.7610057486 | kriging-lin\n", - " 12 | 140 | 7.3640055044 | 7.3640055044 | 6/10 | 1.0600526491 | kriging-const-ARD\n", - " 13 | 150 | 7.0657745472 | 7.0657745472 | 4/10 | 1.2643272170 | kriging-const-ARD\n", - " 14 | 160 | 7.0657745472 | 7.0657745472 | 6/10 | 2.2759640477 | kriging-lin\n", - " 15 | 170 | 7.0407792334 | 7.0407792334 | 3/10 | 2.1436566074 | kriging-lin\n", - " 16 | 180 | 6.9854628698 | 6.9854628698 | 4/10 | 0.3210627737 | kriging-quadr\n", - " 17 | 190 | 6.9847040068 | 6.9847040068 | 2/10 | 1.8313479569 | kriging-lin\n", - " 18 | 200 | 6.9162278041 | 6.9162278041 | 3/10 | 1.2739195852 | kriging-lin\n", - " 19 | 210 | 6.9081087618 | 6.9081087618 | 3/10 | 0.6008899786 | kriging-lin\n", - " 20 | 220 | 6.8826264252 | 6.8826264252 | 6/10 | 0.6377193155 | kriging-lin\n", - " 21 | 230 | 6.8815544475 | 6.8815544475 | 5/10 | 0.6552357623 | kriging-lin\n", - " 22 | 240 | 6.8403116305 | 6.8403116305 | 6/10 | 0.0724192352 | kriging-quadr\n", - " 23 | 250 | 6.8274044511 | 6.8274044511 | 3/10 | 0.4952315643 | kriging-lin\n", - " 24 | 260 | 6.8274035650 | 6.8274035650 | 7/10 | 0.5406026626 | kriging-lin\n", - " 25 | 270 | 6.8030346170 | 6.8030346170 | 5/10 | 0.4775624871 | kriging-lin\n", - " 26 | 280 | 6.8009722666 | 6.8009722666 | 1/10 | 0.4409766638 | kriging-lin\n", - " 27 | 290 | 5.2446639674 | 5.2446639674 | 4/10 | 1.043396E+01 | kriging-lin\n", - " 28 | 300 | 5.1590950227 | 5.1590950227 | 2/10 | 1.336450E+01 | kriging-quadr\n", "Best solution found: \n", - "X = [-2.00803163 1.86061669 -2.09794872 1.95560802 -0.14461223 0.08631508\n", - " 0.07473123 -0.07630844 0.89663685 -0.25277987]\n", - "F = [5.15909502]\n", + "X = [ 0.21016743 0.12239953 0.06492915 0.14055747 -0.08218127 -0.29199435\n", + " -0.81993029 0.84808621 0.03417437 0.87889052]\n", + "F = [2.77086036]\n", "CV=[0.]\n" ] } @@ -886,48 +883,30 @@ { "cell_type": "code", "execution_count": 6, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-05T19:34:07.051396Z", + "iopub.status.busy": "2026-07-05T19:34:07.051260Z", + "iopub.status.idle": "2026-07-05T19:34:28.624285Z", + "shell.execute_reply": "2026-07-05T19:34:28.623916Z" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "================================================================================================================================================\n", - "n_gen | n_eval | f_min | f_gap | n_influenced | mae | model \n", - "================================================================================================================================================\n", - " 1 | 30 | 2.033913E+01 | 2.033913E+01 | - | 0.2584492905 | RBF[kernel=mq,tail=linear,normalized=True]\n", - " 2 | 40 | 1.489029E+01 | 1.489029E+01 | 5/10 | 0.4320034794 | RBF[kernel=mq,tail=linear,normalized=True]\n", - " 3 | 50 | 1.489029E+01 | 1.489029E+01 | 6/10 | 0.5599724717 | RBF[kernel=linear,tail=linear,normalized=True]\n", - " 4 | 60 | 1.203982E+01 | 1.203982E+01 | 8/10 | 0.7991257442 | RBF[kernel=linear,tail=linear,normalized=True]\n", - " 5 | 70 | 1.203982E+01 | 1.203982E+01 | 5/10 | 0.8959677659 | RBF[kernel=linear,tail=constant,normalized=True]\n", - " 6 | 80 | 1.203982E+01 | 1.203982E+01 | 5/10 | 1.0550719580 | RBF[kernel=linear,tail=constant,normalized=True]\n", - " 7 | 90 | 1.190403E+01 | 1.190403E+01 | 4/10 | 0.6643174886 | kriging-const\n", - " 8 | 100 | 8.7732279452 | 8.7732279452 | 7/10 | 0.7612524061 | kriging-const\n", - " 9 | 110 | 8.7732279452 | 8.7732279452 | 6/10 | 0.6504482134 | kriging-const\n", - " 10 | 120 | 8.7732279452 | 8.7732279452 | 7/10 | 0.6183165691 | kriging-const\n", - " 11 | 130 | 7.4950684256 | 7.4950684256 | 8/10 | 0.6255644662 | kriging-const\n", - " 12 | 140 | 7.4950684256 | 7.4950684256 | 7/10 | 0.6872365051 | kriging-const\n", - "BIASED: TOO CLOSE (SKIP)\n", - " 13 | 150 | 7.4950684256 | 7.4950684256 | 8/10 | 0.5375685411 | kriging-const\n", - " 14 | 160 | 6.4510590085 | 6.4510590085 | 4/10 | 0.4939197965 | kriging-const-ARD\n", - " 15 | 170 | 5.9600945536 | 5.9600945536 | 7/10 | 0.4638647655 | kriging-quadr\n", - " 16 | 180 | 5.9600945536 | 5.9600945536 | 7/10 | 0.4446192503 | kriging-const-ARD\n", - " 17 | 190 | 5.4886456424 | 5.4886456424 | 7/10 | 0.4839298024 | kriging-const-ARD\n", - " 18 | 200 | 5.4886456424 | 5.4886456424 | 9/10 | 0.6993539907 | kriging-lin-ARD\n", - " 19 | 210 | 5.4886456424 | 5.4886456424 | 6/10 | 0.7372165210 | kriging-lin-ARD\n", - " 20 | 220 | 5.4886456424 | 5.4886456424 | 8/10 | 0.7630410210 | kriging-lin-ARD\n", - " 21 | 230 | 5.4886456424 | 5.4886456424 | 7/10 | 0.6797742577 | kriging-quadr-ARD\n", - " 22 | 240 | 5.2455941969 | 5.2455941969 | 5/10 | 0.7468197492 | kriging-lin-ARD\n", - " 23 | 250 | 3.8890189117 | 3.8890189117 | 5/10 | 0.5426021921 | kriging-lin-ARD\n", - " 24 | 260 | 3.8890189117 | 3.8890189117 | 3/10 | 0.4366853768 | kriging-lin-ARD\n", - " 25 | 270 | 3.8890189117 | 3.8890189117 | 5/10 | 0.3746281150 | kriging-const-ARD\n", - " 26 | 280 | 3.8890189117 | 3.8890189117 | 7/10 | 0.4238507263 | kriging-const-ARD\n", - " 27 | 290 | 3.8890189117 | 3.8890189117 | 5/10 | 0.3439499037 | kriging-quadr-ARD\n", - " 28 | 300 | 3.8890189117 | 3.8890189117 | 4/10 | 0.5077978046 | kriging-const-ARD\n", + "INFLUENCED: TOO CLOSE\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ "Best solution found: \n", - "X = [-0.73853388 0.20019719 0.29418694 -0.68726892 0.33516822 0.03035722\n", - " -0.48582207 -0.93401104 0.81073807 -0.38538059]\n", - "F = [3.88901891]\n", + "X = [-1.45464442 -0.32436398 -0.98202601 -1.3262732 1.6513295 0.01291417\n", + " 5.05262665 1.30432567 3.48213406 2.51796996]\n", + "F = [9.28609043]\n", "CV=[0.]\n" ] } @@ -964,44 +943,24 @@ { "cell_type": "code", "execution_count": 7, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-05T19:34:28.627442Z", + "iopub.status.busy": "2026-07-05T19:34:28.627298Z", + "iopub.status.idle": "2026-07-05T19:36:50.616576Z", + "shell.execute_reply": "2026-07-05T19:36:50.615715Z" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "=================================================================================================\n", - "n_gen | n_eval | cv_min | cv_avg | f_min | f_gap | n_influenced\n", - "=================================================================================================\n", - " 1 | 27 | 1.714499E+02 | 5.617944E+02 | - | - | -\n", - " 2 | 28 | 1.7247367719 | 5.617944E+02 | - | - | 1/1\n", - " 3 | 29 | 0.000000E+00 | 5.617944E+02 | -6.400043E+00 | 8.5999570206 | 1/1\n", - " 4 | 30 | 0.000000E+00 | 5.617944E+02 | -1.079833E+01 | 4.2016695983 | 1/1\n", - " 5 | 31 | 0.000000E+00 | 5.617944E+02 | -1.155193E+01 | 3.4480660762 | 1/1\n", - " 6 | 32 | 0.000000E+00 | 5.617944E+02 | -1.304470E+01 | 1.9552979195 | 1/1\n", - " 7 | 33 | 0.000000E+00 | 5.617944E+02 | -1.454634E+01 | 0.4536595946 | 1/1\n", - " 8 | 34 | 0.000000E+00 | 5.617944E+02 | -1.480047E+01 | 0.1995280575 | 1/1\n", - " 9 | 35 | 0.000000E+00 | 5.617944E+02 | -1.491833E+01 | 0.0816667392 | 1/1\n", - " 10 | 36 | 0.000000E+00 | 5.617944E+02 | -1.495020E+01 | 0.0498020785 | 1/1\n", - " 11 | 37 | 0.000000E+00 | 5.617944E+02 | -1.497953E+01 | 0.0204729495 | 1/1\n", - " 12 | 38 | 0.000000E+00 | 5.617944E+02 | -1.498599E+01 | 0.0140111011 | 1/1\n", - " 13 | 39 | 0.000000E+00 | 5.617944E+02 | -1.499666E+01 | 0.0033414569 | 1/1\n", - " 14 | 40 | 0.000000E+00 | 5.617944E+02 | -1.499798E+01 | 0.0020232532 | 1/1\n", - " 15 | 41 | 0.000000E+00 | 5.617944E+02 | -1.499893E+01 | 0.0010657277 | 1/1\n", - " 16 | 42 | 0.000000E+00 | 5.617944E+02 | -1.499951E+01 | 0.0004923939 | 1/1\n", - " 17 | 43 | 0.000000E+00 | 5.617944E+02 | -1.499989E+01 | 0.0001121181 | 1/1\n", - " 18 | 44 | 0.000000E+00 | 5.617944E+02 | -1.499996E+01 | 0.0000392967 | 1/1\n", - " 19 | 45 | 0.000000E+00 | 5.617944E+02 | -1.499998E+01 | 0.0000213215 | 1/1\n", - " 20 | 46 | 0.000000E+00 | 5.617944E+02 | -1.499999E+01 | 6.450720E-06 | 1/1\n", - " 21 | 47 | 0.000000E+00 | 5.617944E+02 | -1.500000E+01 | 2.063368E-06 | 1/1\n", - " 22 | 48 | 0.000000E+00 | 5.617944E+02 | -1.500000E+01 | 1.004444E-06 | 1/1\n", - " 23 | 49 | 0.000000E+00 | 5.617944E+02 | -1.500000E+01 | 7.880460E-07 | 1/1\n", - " 24 | 50 | 0.000000E+00 | 5.617944E+02 | -1.500000E+01 | 3.512184E-07 | 1/1\n", "Best solution found: \n", - "X = [0.99999999 1. 1. 0.99999999 0.99999996 0.99999999\n", - " 0.99999999 0.99999999 1. 2.99999992 2.99999995 2.99999995\n", - " 1. ]\n", - "F = [-14.99999965]\n", + "X = [0.43516493 0.32104147 0.52684658 0.93238642 0.27053718 0.70789495\n", + " 0.63993948 0.80099286 0.3741194 2.13507939 1.94684919 1.79998037\n", + " 0.64073612]\n", + "F = [-5.4356714]\n", "CV=[0.]\n" ] } @@ -1041,45 +1000,19 @@ { "cell_type": "code", "execution_count": 8, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-05T19:36:50.618162Z", + "iopub.status.busy": "2026-07-05T19:36:50.617860Z", + "iopub.status.idle": "2026-07-05T19:37:05.836677Z", + "shell.execute_reply": "2026-07-05T19:37:05.836290Z" + } + }, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "==========================================================================================================================\n", - "n_gen | n_eval | n_nds | igd | gd | hv | n_influenced | mae f1 | mae f2 \n", - "==========================================================================================================================\n", - " 1 | 21 | 3 | 1.8942829534 | 3.0095912826 | 0.000000E+00 | - | 2.158343E-16 | 0.1648489778\n", - " 2 | 31 | 3 | 0.4620001734 | 0.7493117302 | 0.1510010190 | 4/10 | 2.875825E-16 | 0.1602687910\n", - " 3 | 41 | 2 | 0.3371490822 | 0.2924047767 | 0.3268413114 | 2/10 | 3.001494E-16 | 0.1174748180\n", - " 4 | 51 | 8 | 0.1408073065 | 0.2210541972 | 0.4390090282 | 4/10 | 3.452678E-16 | 0.0959817810\n", - " 5 | 61 | 10 | 0.1408073065 | 0.0554779978 | 0.4390090282 | 2/10 | 3.650537E-16 | 0.0920210809\n", - " 6 | 71 | 16 | 0.1221342838 | 0.0449904710 | 0.4557288877 | 4/10 | 4.895776E-16 | 0.0935525354\n", - " 7 | 81 | 27 | 0.0827579407 | 0.0468689921 | 0.5246360468 | 4/10 | 4.549254E-16 | 0.0804396406\n", - " 8 | 91 | 43 | 0.0665862375 | 0.0457088275 | 0.5597220040 | 5/10 | 5.034923E-16 | 0.0734327256\n", - " 9 | 101 | 47 | 0.0619377292 | 0.0447108249 | 0.5713835331 | 5/10 | 4.815151E-16 | 0.0677243980\n", - " 10 | 111 | 61 | 0.0388290475 | 0.0363894044 | 0.6033660247 | 8/10 | 5.531123E-16 | 0.0557135910\n", - " 11 | 121 | 66 | 0.0346932634 | 0.0310968683 | 0.6118625353 | 6/10 | 5.023857E-16 | 0.0514084161\n", - " 12 | 131 | 71 | 0.0289701336 | 0.0298619940 | 0.6199182144 | 6/10 | 6.586745E-16 | 0.0580864746\n", - " 13 | 141 | 80 | 0.0259409226 | 0.0264607872 | 0.6244944372 | 5/10 | 6.353598E-16 | 0.0663689181\n", - " 14 | 151 | 90 | 0.0250694448 | 0.0228440028 | 0.6262433378 | 5/10 | 5.765809E-16 | 0.0502260611\n", - " 15 | 161 | 99 | 0.0244332270 | 0.0232342003 | 0.6281135848 | 8/10 | 5.182270E-16 | 0.0512215780\n", - " 16 | 171 | 106 | 0.0242365158 | 0.0192595759 | 0.6285037545 | 8/10 | 3.970392E-16 | 0.0534405466\n", - " 17 | 181 | 107 | 0.0223183843 | 0.0149311969 | 0.6310924941 | 3/10 | 5.725975E-16 | 0.0440707098\n", - " 18 | 191 | 123 | 0.0218602416 | 0.0143220756 | 0.6318522249 | 6/10 | 8.820722E-16 | 0.0486144418\n", - " 19 | 201 | 138 | 0.0211919510 | 0.0138804874 | 0.6332318400 | 6/10 | 1.540363E-15 | 0.0497609611\n", - " 20 | 211 | 155 | 0.0211090608 | 0.0133719333 | 0.6333551695 | 6/10 | 1.865587E-15 | 0.0442893928\n", - " 21 | 221 | 166 | 0.0203041406 | 0.0127681633 | 0.6344643397 | 8/10 | 1.988264E-15 | 0.0582482969\n", - " 22 | 231 | 171 | 0.0200470557 | 0.0120744782 | 0.6352276331 | 5/10 | 2.353805E-15 | 0.0773347096\n", - " 23 | 241 | 173 | 0.0199378688 | 0.0113533889 | 0.6358860735 | 5/10 | 2.550337E-15 | 0.0800614458\n", - " 24 | 251 | 192 | 0.0196526487 | 0.0121477068 | 0.6363797924 | 9/10 | 3.551637E-15 | 0.0640281394\n" - ] - }, { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 8, @@ -1088,7 +1021,7 @@ }, { "data": { - "image/png": "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WdK6+kRERMTjaLovTfeVf7y9zdkPgAzgfWARYAOeBG4q3H/0RERExEma7kusl5EBtWsD4A0MAjoDho8PY7p3Z+rUqVZWJyIiIh5GwVby15YtjjlsbYbBY1lZ3PrppwB88cUX/PjjjxTyXxqIiIhIHlGwlQJls9m477776Nu3LwA//PADX3/9tcKtiIiIXDMFWylwNpuN6OhoHnzwQQCmTJnCBx98QOa/43FFREREroaCrVimZ8+ePP3009jtdubNm8ebb75JWlqa1WWJiIiIm1KwFUtFRUUxbNgwfH19Wb16NS+++CJJSUlWlyUiIiJuSMFWLNe8eXPeeOMNihYtypYtW3j++eeJj4+3uiwRERFxMwq24hLq1q3L22+/TalSpdi3bx/PPfcc+/bts7osERERcSMKtuIyqlSpwqhRo6hcuTLx8fE899xzbNy40eqyRERExE0o2IpLKVOmDO+88w516tQhOTmZl156iUWLFlldloiIiLgBBVtxOUFBQbz55pu0bt2ajIwMRo8ezS+//KK5bkVEROSSFGzFJfn6+jJkyBBuueUWAL755hs+/vhjzXUrIiIiF6VgKy7LZrPxwAMP8PDDD2Oz2Zg9ezavv/46Z86csbo0ERERcUEKtuLyevTo4ZjrNi4ujueff55jx45ZXZaIiIi4GAVbcQstWrRgxIgRFC9enF27dvF///d/bN261eqyRERExIUo2IrbuP7663n33XepVq0ap06d4oUXXiA2NtbqskRERMRFKNiKW8meDqx58+akp6fzzjvv8OOPP2rGBBEREXG9YDtlyhRCQ0MJDw8nIiKCTZs2XbL9zp07ue2222jfvj316tWjZcuWrFmzpoCqFSsUKVKEYcOGceuttwLwww8/MHr0aNLT0881OnEC+vSBJk2gWzfYu9eiakVERKSguFSwXbVqFf3792fChAnExsbywAMP0KlTJ5KSknJtf+zYMTp06MDTTz/NggULWL9+PQEBAezYsaOAK5eCZrfbuf/++3nyySfx8vJi8eLFDBkyhPj4eDPIlioFv/wCf/4JM2dC1arQqBFoujARERGP5VLBduTIkXTr1o1atWoB0K9fPzIyMhg/fnyu7d9++21atWpF27ZtAfD29uazzz5zvBfP17FjR15//XWCgoLYvn07zzRpwuaZM8HfH957DwwDfvwRSpSADRugaVOr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V155hQ4dOhAQEEBSUhIjR47kqaeesuorFGrp6el07NjRcXNmdHQ0ISEhjikuU1NTc9zYV7VqVaZPn86gQYPw9fUlLS2N6dOnU7Vq1QKv3WYYhlHgZxURERERyWMaYysiIiIiHkHBVkREREQ8goKtiIiIiHgEBVsRERER8QgKtiIiIiLiERRsRURERMQjKNiKiIiIiEdQsBURERERj6BgKyIiIiIeQcFWRMRDfPjhh1StWhVvb2+effZZq8sRESlwWlJXRMQDrF+/nmbNmjF16lQaN25McHAwAQEBVpclIlKgvK0uQERErt306dNp3rw5Xbt2tboUERHLKNiKiLi5mjVr8s8//wBgs9m4++67+fbbby2uSkSk4GkogoiImzt69CitWrXi0UcfpV+/fhQtWpSiRYtaXZaISIHTzWMiIm6uaNGi7N69mzZt2lC+fHnuvvtuSpQoQe/eva0uTUSkQCnYioi4uQ0bNgDQoEEDAJ5++mkNRRCRQknBVkTEza1bt46aNWsSGBgIQLt27QgKCrK4KhGRgqdgKyLi5tatW0ejRo2sLkNExHIKtiIibm7dunXceOONVpchImI5BVsRETeWlZXFX3/9pR5bERE0j62IiFuz2+0kJydbXYaIiEvQPLYiIh4mKiqK9evXk5ycTMmSJZk0aRKtWrWyuiwRkXynYCsiIiIiHkFjbEVERETEIyjYioiIiIhHULAVEREREY+gYCsiIiIiHkHBVkREREQ8goKtiIiIiHgEBVsRERER8QgKtiIiIiLiERRsRURERMQjKNiKiIiIiEdQsBURERERj6BgKyIiIiIe4f8BEuTWsMV5xJQAAAAASUVORK5CYII=", + "image/png": "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c+GFFyoiIkI1NTWSpMcee0wzZszQhx9+qLy8PN19990yDEOTJ0+26tsAAACARSy7AWzHjh269tprVVVVpeTkZJe+v/3tb87XoaGhateunXNO7CWXXKLp06frmWee0a5du5SSkqJ58+apY8eOXq0fAAAA1nMYJ95N1QqUlpYqIiJCJSUlTDkAAABohhqb1yyfMwsAAACcKsIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2CLMAAACwLcIsAAAAbIswCwAAANsizAIAAMC2mkWYfe+995SUlKSKiopfPPaf//ynEhMTXR5Dhw71QpUAAABobvysvHhVVZV+//vf64svvtD27dtVU1Pzi+cUFRXp0UcfdQmwvr6+niwTAAAAzZSlYfabb77R0KFDNXz4cF177bWNPq9Hjx5KTEz0YGUAAACwA0unGVx66aW67rrrTvq8uXPnaty4cRo2bJgmT56sHTt2eKA6AAAANHeWjsyeqqVLl+qhhx5SaGiopk2bptTUVK1fv16dOnWq9/jy8nKVl5c735eWlnqrVAAAAHhQs7gB7GTccccdmj17ti644AKNGjVK//73v+VwOPTaa681eM7UqVMVERHhfERFRXmxYgAAAHiK7cJs+/btFRgY6HwfFBSkpKQkbdq0qcFzpkyZopKSEucjPz/fG6UCAADAw2wXZs8///w6bbt371bnzp0bPCcwMFDh4eEuDwAAANhfsw+zv/71r5WSkiLDMCRJq1at0ssvv+zs/+c//6ns7Gxdc801FlUIAAAAq1h6A9jOnTs1duxYHTx4UJKUnJysPn366PPPP3ces3nzZq1bt06GYcjhcOiVV17RM888o2eeeUaS5O/vr/fee0+jRo2y5HsAAACAdRxG7ZCnBSorK5WTk+PSFhQUpOjoaOf7PXv26PDhw4qJiXE57sCBA6qsrGxwBQN3SktLFRERoZKSEqYcAAAANEONzWuWjsz6+/v/4uYHDc2FbdeunSdKAgAAgI00+zmzAAAAQEMIswAAALAtwiwAAABsizALAAAA2yLMAgAAwLYIswAAALAtwiwAAABsizALAAAA2yLMAgAAwLYIswAAALAtwiwAAABsizALAAAA2yLMAgAAwLYIswAAALAtwiwAAABsizALAAAA2yLMAgAAwLYIswAAALAtwqynVFRInTtLDofr47LLrK4MAACgxSDMekJFhRQYKBUWmu/9/SWf//2n/uILqXt3y0oDAABoSQizntCpk/kcESEZhhluq6ul7GyzfedO6d13LSsPAACgpSDMekJJiflcXOza3quX9Oab5us77/RqSQAAAC0RYbap7d9vPoeE1N9/113mc0WFd+oBAABowQizTS0gwHyuqrK2DgAAgFaAMNvUQkPN5/Ly+kdfb7vNfA4P915NAAAALRRh1hMSEsznoCBp7dpj7Y88Is2YYb6eN8/7dQEAALQwflYX0CJt3iwFB0tHjkjJyXX7L75YGjzY+3UBAAC0MIzMesrhw9JNN5kbJdRq00b6+mvpyy+tqwsAAKAFcRiGYVhdhLeVlpYqIiJCJSUlCmfuKgAAQLPT2LzGyCwAAABsizALAAAA2yLMAgAAwLYIswAAALAtwiwAAABsizALAAAA2yLMAgAAwLYIswAAALAtwiwAAABsizALAAAA2yLMAgAAwLYIswAAALAtwiwAAABsizALAAAA2yLMAgAAwLYIswAAALAtwiwAAABsizALAAAA2yLMAgAAwLYIswAAALAtwiwAAABsizALAAAA2yLMAgAAwLYIswAAALAtwiwAAABsizALAAAA2yLMAgAAwLYIswAAALAtwiwAAABsizALAAAA2yLMAgAAwLYIswAAALAtwiwAAABsizALAAAA2yLMekplpdSrl+RwHHv4+EgXXmh1ZQAAAC3GaYfZTz75RL/61a/09ttvO9vmzJmjZ5999nS/tH1VVkpt2kg5Oeb70FApKEgyDGnOHKlLF2vrAwAAaCFOK8xu27ZNt99+u8aPH6+VK1fq/vvvlyTt3r1bmzZtapICbalPH6m6WoqIkGJipLIyqbxcCgkx+/fskf78Z2trBAAAaAFOK8yuWLFC5513nm699Va9+eabOuOMM/T44483VW32tWWL+VxSIm3bZo7IGoYZams9/bQ1tQEAALQgjQqzxcXFKi8vr9Pep08fbd682fn+T3/6kwoLC/X3v/+96Sq0u2uvPRZm3333WHtlpWUlAQAAtBSNCrNvv/22/vjHP0qSdu3apf3790uS+vbtqwsuuEBz5sxxHvvGG28oOjpaHTt29EC5NvPrX0sffHDs/U03SQUF1tUDAADQwvg15qDw8HDl/O9mpn//+9/avXu3XnzxRUnSCy+84HKsr6+vZs2a1cRl2tRf/1q37aOPvF8HAABAC9WokdkRI0Zo1qxZ+tvf/uYMtWiETp2khQvN119+KZ11lvTAA9bWBAAA0II4DMMwGnPgBx98oOeee05r166Vr6+vEhISlJycrJSUFOdzF5ssOVVaWqqIiAiVlJQoPDy86S8QENC4ObGN+08PAADQ6jQ2rzV6NYP/+7//05o1a/Tcc8/pkksu0V133aXg4GD95z//0WWXXaauXbuqc+fOeuONN5rkG7C1J56ov93fX/L1PfZ+0iTv1AMAANBCNXpktlZOTo727t2rYcOGOduqqqq0efNmrV69Wp06ddL555/f5IU2JY+PzEpShw7S/26UU2io+di9u+5xR46YGyoAAADAqclHZmvFxcW5BFlJ8vPzU9++fXXdddc1+yDrNb/97bHXhw4dC7Jt20rFxdI115jvr7rKy4UBAAC0HKe9nS0aMHeu+fyvf5nLcWVmmnNkDxwwdwZ7/32zf90662oEAACwOcKsp3TrZj7PnSt17Sr16+fa/69/mc8REd6tCwAAoAU5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WfWf8J//SP36mXNqAQAAWjnCbDPQrl07/fa3v5UkfbVnjzI6dDBv9jpw4NhBhmHOmf38c+k3vzk27QAAAKAVI8w2E4MHD9akSZMkHx9NGzJEhRs2mDuB3X67NGWKNHiwdOut0h13mG0AAAAgzDYnN998s6Kjo1Xi56fHJk5U8eTJ0rJl0kcfSZGR5hJd/+//MSoLAADwP4TZZiQgIEBPPPGEOnXqpIKSEj1+9KjKli+Xtm41N02YMIEgCwAAcBzCbDPTsWNHPf3002rbtq22bdump556SkePHrW6LAAAgGaJMNsMde3aVX/5y18UEhKijRs36q9//asqKyutLgsAAKDZIcw2U9HR0XryyScVGBioVatW6cUXX1R1dbXVZQEAADQrhNlmLDExUX/605/k5+enJUuW6NVXXyXQAgAAHIcw28ylpKTo4YcflsPh0Pz58zV16lSVl5dbXRYAAECzQJi1geHDh+vRRx+Vv7+/li9frscee0wHDx60uiwAAADLEWZtYtiwYXr66aedN4X94Q9/0N69e60uCwAAwFKEWRvp06ePnn/+eXXs2FH5+fl66KGHlJeXZ3VZAAAAliHM2kyPHj30wgsvKCoqSkVFRfrDH/6gtWvXWl0WAACAJQizNtSxY0c999xzSkpKUllZmR577DF9+eWXMgzD6tIAAAC8ijBrU2FhYXr66ac1ZswY1dTU6O2339ZLL73ESgcAAKBVIczaWEBAgB544AHdcccd8vHx0YIFC/Tggw9q9+7dVpcGAADgFYRZm3M4HJo0aZKeeeYZRUREKDc3Vw888IAyMjKsLg0AAMDjCLMtRL9+/fTyyy+rd+/eOnTokJ566im9//77qqqqsro0AAAAjyHMtiAdO3bU1KlTdcEFF8gwDH388cf6wx/+oF27dlldGgAAgEcQZlsYf39/3XPPPXrkkUcUEhKirKws/eY3v9H8+fNZ7QAAALQ4hNkWauTIkXr99dfVr18/HT16VNOmTdOLL76osrIyq0sDAABoMoTZFqxjx4565plndMMNN8jHx0c///yz7r33Xq1atcrq0gAAAJqEw2iFv3suLS1VRESESkpKFB4ebnU5XrF582a9+OKLzmW7zj//fN12220KCQmxuDIAAIC6GpvXGJltJXr37q3XXntNkyZNksPh0A8//KB77rlH6enpVpcGAABwyhiZbSUjs8fbsGGDXnnlFRUUFEiSxowZo1//+tcKCwuzuDIAAAATI7NoUJ8+ffTqq6/qsssuk8Ph0I8//qjJkydr3rx5rHgAAABshZHZVjgye7zNmzfr1Vdf1fbt2yWZmy/cdddd6tGjh8WVAQCA1qyxeY0w28rDrCRVVVXpyy+/1KxZs1ReXi5fX19ddtll+tWvfqWgoCCrywMAAK0QYdYNwmz9CgsL9Y9//EPLly+XJHXq1Em33Xabhg8fLofDYXF1AACgNSHMukGYdW/58uX6+9//rr1790oypx7ccccdiomJsbgyAADQWhBm3SDM/rKjR4/q008/1WeffaaKigo5HA6NHz9e119/vSIiIqwuDwAAtHCEWTcIs41XWFiod955R4sWLZIkhYSE6JprrtFFF10kf39/a4vbu1datkwyDGnYMKlTJ2vrAQAATYYw6wZh9uStX79e//jHP7R161ZJ5nzaG2+8UWeddZb359MeOiTdf780c6ZUUWG2+ftL114rvfaaxGcKAIDtEWbdIMyempqaGs2bN08zZ87U/v37JUlxcXG65ZZblJyc7J0iKiulsWOl1aulxx6TrrlGcjikjz+W/vxnKSlJ+uknKTDQO/UAAACPIMy6QZg9PeXl5fryyy/1ySef6MiRI5KkQYMG6aabbvL8TWIffCBdd520cKE0apRrX1qaOd1gxgzp5ps9WwcAAPAowqwbhNmmUVJSoo8++kizZ89WdXW1JGnUqFG6/vrrFRkZ6ZmLjhsnVVVJ8+fX33/hhdLhw9KCBZ65PgAA8Aq2s4XHRURE6I477tBbb72l0aNHS5IWLVqku+66Sy+//LIKCwub/qI7dkjupjQMGGAeAwAAWgVGZhmZbTLbtm3TzJkzlZaWJkny8/PTuHHjdPXVV6tDhw5Nc5Fzz5UCAqQ5c+rvv/hiqahIWry4aa4HAAAswTQDNwiznrV582bNnDlTq1evlnQs1F511VXq2LHj6X3xd9+Vbr1VSk+XBg1y7Vu7VkpNld56S7rjjtO7DgAAsBRh1g3CrHesXbtWs2bN0rp16ySZofb888/XVVddpTPOOOPUvujRo+aNX9u2SX/9q/SrX0k+PuZqBo8+KnXrJi1ZIgUHN+F3AgAAvI0w6wZh1rsyMzP1wQcfuITa8847T1dccYW6dOly8l/wwAFz5PWzz6SaGrPN4ZAuuUR6+22pqaY0AAAAyxBm3SDMWmPdunWaNWuW1q5dK0lyOBw666yzdOWVVyo6Ovrkv2BenrRokbkD2MiRkqeXBQMAAF5DmHWDMGutDRs26OOPP1ZGRoaz7cwzz9RVV12l3r17W1hZI6xeba6WkJIide9udTUAALRYhFk3CLPNw9atW/Wf//xHixcvVu3/hv369dPll1+uwYMHe3+bXHdeekn6y1+kkpJjbbGx0ocfSkOGWFcXAAAtFGHWDcJs87Jz5059+umnmj9/vnPzhR49eujyyy/X2WefLT8/P2sLfPhh6YUXpNBQ6YYbpH79pK+/lr7/3uxftEgaPtzaGgEAaGEIs24QZpunffv26auvvtKcOXOc2+S2b99eF198sS644AKFhIR4v6jiYql9e6lrV3MFhYCAY30rV5qjslFRUm6u92sDAKAFI8y6QZht3srKyjRnzhx99dVX2r9/vyQpKChI559/viZNmqSuXbt6r5j77pNef90chT3//Lr9l14qffmllJUlxcd7ry4AAFo4wqwbhFl7qKys1IIFC/T5559r+/btkswVEIYOHapLL71Uffv29fy82jFjpJ9/lv43/aGOmTPNqQczZ0rXXefZWgAAaEUam9csnowINMzf31/nnXeexo4dqzVr1uiLL75QRkaGli9fruXLlys2NlYXX3yxRo8erYDjf/3flNq2Ndey3b1bqm9N3A0bzOdTWS8XAACcNkZmGZm1lfz8fH399deaN2+eKioqJEnh4eEaP368JkyYcPrb5Z5o8WJzx7HLL5c+/dS1r6ZGOuMM6cgR6dAhcyeyppaTI735pjR3rjk6PHy4dM895tJgAAC0YEwzcIMwa38HDx7U999/r2+//VZ79+6VJPn4+GjYsGGaNGlS005BSEqSNm2SrrrKDJYdO0rp6ea0guxsacoUc2vdpvb11+Y1Q0LMMB0QIH31lbRzpzmP9+67m/6aAAA0E4RZNwizLUd1dbXS0tL09ddfKzMz09keFRWliy66SGPGjFFwcPDpXeTQIWngQDO4SpKfn1RVZb6+5RZpxozT+/r12bHDvKHswgvN+bi130NVlfTgg9Irr0hLl0rDhjX9tQEAaAYIs24QZlum3NxcffPNN/rpp59UXl4uyVwFYcyYMZowYcKpbZl7vPnzpeefN5frSkiQnn1W6tbttOuu12OPmYF1504pLMy1r6ZG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", "text/plain": [ "
" ] @@ -1140,42 +1073,19 @@ { "cell_type": "code", "execution_count": 9, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-05T19:37:05.844354Z", + "iopub.status.busy": "2026-07-05T19:37:05.844206Z", + "iopub.status.idle": "2026-07-05T19:37:42.170179Z", + "shell.execute_reply": "2026-07-05T19:37:42.169655Z" + } + }, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "==========================================================================================================================\n", - "n_gen | n_eval | n_nds | cv_min | cv_avg | igd | gd | hv | n_influenced\n", - "==========================================================================================================================\n", - " 1 | 5 | 1 | 0.5717037305 | 7.3349018351 | - | - | - | -\n", - " 2 | 15 | 2 | 0.000000E+00 | 7.3349018351 | 0.2653544470 | 0.0984902580 | 0.0294105975 | 7/10\n", - " 3 | 25 | 6 | 0.000000E+00 | 7.3349018351 | 0.1688552083 | 0.1110745959 | 0.0804323054 | 6/10\n", - " 4 | 35 | 12 | 0.000000E+00 | 7.3349018351 | 0.1626975751 | 0.1069272188 | 0.0812678935 | 4/10\n", - " 5 | 45 | 16 | 0.000000E+00 | 7.3349018351 | 0.1626975751 | 0.1074034919 | 0.0812678935 | 2/10\n", - " 6 | 55 | 7 | 0.000000E+00 | 7.3349018351 | 0.1568291260 | 0.0536053149 | 0.1334154871 | 4/10\n", - " 7 | 65 | 9 | 0.000000E+00 | 7.3349018351 | 0.1448912306 | 0.0645102339 | 0.1336500488 | 4/10\n", - " 8 | 75 | 13 | 0.000000E+00 | 7.3349018351 | 0.1197962927 | 0.0676481356 | 0.1552136458 | 7/10\n", - " 9 | 85 | 16 | 0.000000E+00 | 7.3349018351 | 0.1197962927 | 0.0687271946 | 0.1552136458 | 1/10\n", - " 10 | 95 | 21 | 0.000000E+00 | 7.3349018351 | 0.0893101188 | 0.0667513661 | 0.1983892329 | 9/10\n", - " 11 | 105 | 26 | 0.000000E+00 | 7.3349018351 | 0.0594609081 | 0.0484714727 | 0.2116452805 | 7/10\n", - " 12 | 115 | 36 | 0.000000E+00 | 7.3349018351 | 0.0573628701 | 0.0455119835 | 0.2254828364 | 8/10\n", - " 13 | 125 | 45 | 0.000000E+00 | 7.3349018351 | 0.0571111062 | 0.0453376673 | 0.2258291227 | 5/10\n", - " 14 | 135 | 59 | 0.000000E+00 | 7.3349018351 | 0.0555906801 | 0.0455882251 | 0.2316224246 | 6/10\n", - " 15 | 145 | 72 | 0.000000E+00 | 7.3349018351 | 0.0555906801 | 0.0462793825 | 0.2316224246 | 4/10\n", - " 16 | 155 | 81 | 0.000000E+00 | 7.3349018351 | 0.0550140152 | 0.0473746175 | 0.2332016113 | 9/10\n", - " 17 | 165 | 90 | 0.000000E+00 | 7.3349018351 | 0.0545351583 | 0.0479898213 | 0.2335873099 | 6/10\n", - " 18 | 175 | 102 | 0.000000E+00 | 7.3349018351 | 0.0544481014 | 0.0472218443 | 0.2338235521 | 9/10\n", - " 19 | 185 | 95 | 0.000000E+00 | 7.3349018351 | 0.0508618111 | 0.0389046008 | 0.2379154517 | 9/10\n", - " 20 | 195 | 92 | 0.000000E+00 | 7.3349018351 | 0.0498523756 | 0.0336684226 | 0.2382643368 | 5/10\n", - " 21 | 205 | 103 | 0.000000E+00 | 7.3349018351 | 0.0495946407 | 0.0339564545 | 0.2388966697 | 2/10\n" - ] - }, { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 9, @@ -1184,7 +1094,7 @@ }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAArYAAAIQCAYAAAB0Ri0fAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAABSiklEQVR4nO3dd3hUZeL28XsmZZJAEnoTJCABFSQonUivS1FRRBQU3fUVXJXqT0Fk3ZVVWEFExLKKggVBXRdZiiIgCCo1oasIUoz0moSE9PP+MSYQksCkzJwzM9/Pdc01M+ecmbnjAbl5eOY5NsMwDAEAAABezm52AAAAAKAsUGwBAADgEyi2AAAA8AkUWwAAAPgEii0AAAB8AsUWAAAAPoFiCwAAAJ8QaHYAs+Xk5OjIkSMKDw+XzWYzOw4AAAAuYxiGkpOTVatWLdntRY/L+n2xPXLkiOrUqWN2DAAAAFxFQkKCateuXeR+vy+24eHhkpz/oSIiIkxOAwAAgMslJSWpTp06eb2tKH5fbHOnH0RERFBsAQAALOxq00b58hgAAAB8AsUWAAAAPoFiCwAAAJ9AsQUAAIBPoNgCAADAJ1BsAQAA4BMotgAAAPAJFFsAAAD4BIotAAAAfALFFgAAAD6BYgsAAACfQLEFAACAT6DYAgAAwCdQbAEAAOATKLYAAADwCRRbAAAA+ASKrYcZhmF2BAAAAJ9EsfWgc+fOadiwYfr444914sQJs+MAAAD4lECzA/iTNWvW6OjRo5o/f74WLFigZs2aqUePHmrTpo0CAzkVAAAApWEz/PzfxpOSkhQZGanExERFRES49bMyMjK0YcMGLV++XDt27MjbXrlyZfXu3Vu9evVyewYAAABv42pfo9h6sNhe6tixY1q5cqW+/vprnT17VpIUHBysTp066Y477lCdOnWu/AapqdLq1VLdulKTJh5IDAAAYA6KrYvMKra5MjMz9d1332nRokX69ddfJUk2m01dunTRkCFDVKVKlfwv2LVLatVKunAh//bYWOm77zyUGgAAwHMoti4yu9jmMgxDP/30k7744gutX79eknME97bbbtOAAQNUrlw5ac8e6frrnS+w2ZyjtYmJ0h8jvqpVSzp82KSfAAAAwD0oti6ySrG91J49ezRnzhzt3r1bkhQeHq577rlHvQcNUlBamtS1q7Ry5cUXpKZK5ctLhiEtWCDdc49JyQEAAMoexdZFViy2knMEd/PmzZo7d64SEhKknBxVX7ZM90vqkJMjm82W/wWffCINGiSVKyedP29KZgAAAHdwta+xjq1F2Ww2tWrVSq+99poef/xxVcrI0HFJ00JC9PTTT2v//v35X5A7Spuamn97YqLUuLHkcEhhYdLdd3skPwAAgKcxYmvREdvLpe3Yof/FxOizoCCl9ewpm82mvn37avDgwc75t5mZUnCwZLdL2dnOF/XvL33xReFvyJQFAADgJRix9TEhTZtqoKQ3MzN1a4sWMgxDixcv1l//+ldt3LhR6tzZeWCzZs77Z5+9WGrvuMM5//bkSem665zbBg2STp3y7A8BAADgRozYesmIrSSpY0dp7VrJZtO2v/9dbx05osMJCdL69Wp/9qwellQpJcU55SAw0Dly+/XXUvfu+d+nRw9pxQqpYUPnSgsAAAAWxpfHXORVxVaS6tSRfv9dkpQhab6k/0rKkRRyyy26d8IE3XbbbQoMCpKCgqSMjMLfx2Zz3nJyPBQcAACgZLx2KkJGRobGjRunwMBAHTx48KrHf/fdd2rTpo06duyoNm3aaN26de4PaaaEBOk//5HCwxVss2mo3a7pTZqo0YgRSqtVS3PmzNHjjz+ufZJz5PZK/PvvNAAAwMcEmh3gUgcPHtS9996rhg0bKjv3C1BXcOjQIfXp00dLlixR+/bt9e2336pv377asWOH6tat64HEJrnrLuftD9dJmmoY+uabbzR37lwdPnxY/ydpWGKiehpGwaXBjh513gcEeCwyAACAu1lqxPb8+fP68MMP9dBDD7l0/Kuvvqobb7xR7du3lyR17NhRjRo10syZM90Z05JsNpu6du2qN998U61bt1ZWQIBelzTtxhuVkpKS/+CGDZ33rIoAAAB8iKWKbZMmTdSgQQOXj1+1apVatGiRb1vLli218tKrcl0mPT1dSUlJ+W6+pHz58powYYIemjZNdklrf/5ZI8LD9WO7dtIttziXAzt/3vnlsnnzzI4LAABQZixVbItr//79ql69er5tNWrU0IEDB4p8zeTJkxUZGZl3q1OnjrtjepzNZtOdo0bpX59+quo2m04YhsatX695W7cqyzCkihULXsgBAADAy3l1sU1NTZXD4ci3zeFwKPUKpW38+PFKTEzMuyUkJLg7pmmuv/tuzTx/Xp2ffFJG7dpaEBWlcSNG6Oju3c4VEwAAAHyIVxfbsLAwpaen59uWnp6usCusBuBwOBQREZHv5svCwsI0ZupU/d+8eSrXurX27N+vESNGaMWKFfLzld4AAICP8epiW79+fR0/fjzftmPHjql+/fomJbKuDh066LXXXlOTJk2UlpammTNnatq0aUpLSzM7GgAAQJnw6mLbtWtXxcXF5du2ZcsWdevWzaRE1la1alW98MILGjp0qAICArR27Vr93//9nw4fPmx2NAAAgFLzqmJ733336f777897PnLkSO3evVvff/+9JGndunX6+eef9cQTT5gV0fLsdrsGDBigf/7zn4qMjNTBgwc1atQorV271uxoAAAApWKpCzRkZGSoR48eOnfunCRp0KBBqlOnjj777DNJUlpamuz2i128bt26WrJkicaOHavg4GClp6dryZIlvn1xhjLSpEkTzZw5U1OnTtWuXbs0depU7dixQ4888oiCg4PNjgcAAFBsNsPPv0Hk6rWHfVV2drYWLFigTz75RIZhKCoqSk8//bRq165tdjQAAABJrvc1r5qKgLIXEBCgwYMH6/nnn1eFChV08OBBjR49WmvWrDE7GgAAQLFQbCFJatasmWbOnKmmTZsqLS1NL7/8smbOnFlgOTWlpEgREc4rmDkc0saN5gQGAAC4DFMR/HwqwuVycnK0YMECLViwQIZhqG7dunr66aedV2gLDZWKWh7Mv38ZAQAAN2IqAkrEbrfrvvvu06RJk1ShQgUdOnRIo0eP1qqQkIul1uGQpk2TLr2csc1WdiGOHJECApzvabNJVaqU3XsDAACfRbFFoWJiYjRz5kzFxMQoPT1dM9LT9bKklC+/dBbcsWOlY8ek8+cvvujtt0v/wZUqSddcI+XkXNx2+rSz4A4dWvr3BwAAPotiiyJVrFhRzz//vO6Pi5NN0hpJT3z6qXbt2nXxoHLlpJYtnY+HDy/dBzZvLp0963wcFCQlJUnLll3c/8EH0pYtpfsMAADgs5hjyxzbqwsN1U9paZpeoYKO3XqrbDab7rjjDt1///0KCgpyHpM7FaE0v5xy3+OBB6T338+/z+GQMjJK/xkAAMDrMMcWZefaa3WDpJmJierRo4cMw9DChQv15JNPKiEhQWrVynlcWc2zvbzUStLlqzMAAABchmKLq9uzR5IUahh6olUrPfvss4qIiND+/fs1cuRI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V13497+lnByzUwFAiVBsAaAYBg8erIiICP32229aunSp2XFK5447pJYtpZUrnXN2d+2Shg+XqleXjhwxOx0AFBvFFgCKITw8XA888IAk6eOPP1ZiYqLJiUpo5EjnF9GaN3desjc5WUpLk8aNc170oUULsxMCQLFRbAGgmLp3767rrrtOqampmjdvntlxii8nR3r7bedauJs2Sdde69weGChNniwNGyYdPSr973/m5gSAYqLYAkAx2e12Pfzww5Kkr776SgcPHjQ3UHGtXescnX34YcleyB8D06Y572fN8mwuACglii0AlECTJk0UGxsrwzD07rvvyjAMsyO57uxZ533uSO3lypd3XgTiwgXPZQKAMkCxBYASeuihhxQYGKht27Zp8+bNZsdxXdu2zvv58wvf/7//SdnZ0i23eC4TAJQBii0AlFD16tV1xx13SJLee+8971n+q0YNqXFj55SEyy8RnJQk/fnPzkv2TppkTj4AKCGKLQCUwt13363IyEgdPnxYy5YtMzuO6xYtkoKDpX79nOvXjh3rXP6ralXp9GnphRekiAizUwJAsVBsAaAUwsLCdP/990uS5s+fr+TkZJMTuei666Q9e6Q2baQdO6Tp051lt2pV6aOPpPHjzU4IAMVGsQWAUurevbuioqJ0/vx5zS9q3qoV1a0rrV/vXCFh/34pMVH6/Xdp8GCzkwFAiVBsAaCULl3+a+nSpfr9999NTlRMwcFSvXpMPQDg9Si2AFAGYmJi1KpVK+Xk5Gju3LlmxwEAv0SxBYAy8uCDD8put2vjxo3avXu32XEAwO9QbAGgjNSpU0c9evSQJM2ZM8e7LtoAAD6AYgsAZei+++5TSEiI9uzZo++//97sOADgVyi2AFCGKlasqP79+0tyjtpmZGSYnAgA/AfFFgDK2J133qkqVaroxIkT+uyzz8yO410OH5Z++83sFAC8FMUWAMpYSEhI3vJfn3/+uY4ePWpyIi9w++1SQIBUu7ZzfV27Xbr1Vik72+xkALwIxRYA3KBdu3Zq1qyZMjMz9c477/BFsiuJipL+9z9nmW3XTurQwbm27vffS1WqUG4BuIxiCwBuYLPZNGzYMAUGBmrz5s18kawo//iHdOiQ8xK/mZnOMvvtt86rod16q3TunHTffWanBOAlKLYA4Ca1a9fW3XffLUl66623lJiYaHIiC5o+3Xm/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ApwjuRcuSLffLv3jH+77fC9DsQUAACgBj6yKIEn9+0spKdLAgVJEhBQW5rw4w9at0hdfuPezvQwXaAAAACgBj15SNzRU+uQT93+Ol2PEFgAAoAQ8eklduIRiCwAAUAIeHbGFSyi2AAAAJcCIrfVQbAEAAErAI6sioFgotgAAACXAVATrodgCAACUgMeW+4LLKLYAAAAl4BVzbDMynFctq11bqlBBqldPevll52V6fRDFFgAAoAQsPxXh99+latWkyZOls2edxfbIEenJJ6W6daXUVLMTljmKLQAAQAlYvti2ayclJjpHaFNSpIMHnZfhHTXKWXq7dDE7YZmj2AIAAJSApacifP+9lJAg3XefNGbMxe12u/TKK1KbNtLGjdKJE+ZldAOKLQAAQAlYesR29mzn/csvF75/3Djn/YcfeiaPh1BsAQAASsDS69jmlu2IiML3R0Y6762YvRQotgAAACVg6eW+brvNef+PfxS+/5VX8h/nIyi2AAAAJWDpEduBA52jtdOnS5s359+3cKG0eLFz6a/Gjc3J5yaBZgcAAADwRpYesZWc5bVLF6lVK+n666XrrpN27ZIOHZJCQqRVq8xOWOYYsQUAACgBS6+KIEkdOkg7d0qdOkm//iotXSodO+acfnDggHPE1scwYgsAAFACll4VIdcNN0irV5udwmMYsQUAACiB3GKblZUlwzBMTgOJYgsAAFAiuVMRJIuP2voRii0AAEAJ5I7YShaeZ+tnKLYAAAAlEBAQIJvNJoliaxUUWwAAgBKw2Wz55tnCfBRbAACAErL8kl+SlJ0tPf201KSJ1LSp9NprZidyG4otAABACVm+2H7wgRQcLL30krR7t3Nd2xEjnNu+/97sdGWOYgsAAFBCgYHOSwJYclWE77+Xhg6VcnKc9+np0rlzUq9eUmam1L69dPKk2SnLFMUWAACghCx9Wd0hQ5z3S5dKc+c6R2kjI6Uvv5Sef14yDOnee02NWNYotgAAACVk6akIBw9KERFS794F902cKAUESOvWeTyWO1FsAQAASsjyl9WtWrXofeXKST62mgPFFgAAoIRyi60lR2ztdikhofB92dlScrIUEuLZTG5GsQUAACghS8+xbdFCyshwLvV1uT59nHNsBw/2fC43CjQ7AAAAgLey9IjtkiVSzZrOpb4WLJAee0xKSZHeeEM6dUoKC5PefNPs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FBkZqcTERK//BeCvOIfej3Po/TiHvoHz6P387RxaZlUEAAAAoDQotpdxOBx67rnnCqyPC+/BOfR+nEPvxzn0DZxH7+dv55CpCAAAAPAJjNgCAADAJ1BsAQAA4BMotgAAAPAJFFsAAAD4BL8stgsXLlTLli3Vvn17dezYUbt37y7T4+F+xTknn376qXr06KGuXbuqZcuWuvvuu3Xw4EHPhUWhSvr7atasWbLZbFqzZo17A+KqinsO9+/fr7vuukudO3dW48aN1aZNG23ZssVDaVGU4pzH9PR0jR49WjExMerYsaNat26thQsXejAtLpeRkaFx48YpMDDQpT/bvvvuO7Vp00YdO3ZUmzZttG7dOveH9CTDz2zcuNEIDw83fvnlF8MwDOP99983rrnmGiMpKalMjof7FfecBAUFGV999ZVhGIaRnZ1t3H///UajRo2MtLQ0j2VGfiX9fXX48GHj2muvNSQZq1ev9kBSFKW45/DEiRNGVFSU8e233xqGYRiZmZlG586djfnz53ssMwoq7nl89tlnjaioKOPcuXOGYRhGfHy8ERwcbGzbts1jmXHRgQMHjDZt2hgPPPCAIck4cODAFY8/ePCgERERYaxdu9YwDMNYs2aNERERYRw8eNADaT3D74pt//79jUGDBuU9z87ONqpXr27MnDmzTI6H+xX3nAwYMCDf882bNxuSjB9++MGtOVG0kv6+uvPOO4233nqLYmsBxT2HY8eONe6999582/bu3WscPnzYrTlxZcU9j3379jXuvvvufNuqVq1qTJ8+3a05UbidO3cae/fuNVavXu1SsR09erTRpk2bfNtatmxpjBkzxo0pPcvvpiKsWrVKLVq0yHtut9vVvHlzrVy5skyOh/sV95x89tln+Z6HhIRIcv6TGsxRkt9XixcvVlBQkHr27OmJiLiK4p7D//73v+rQoUO+bQ0aNFCtWrXcmhNXVtzzeNddd2ndunX67bffJEnLly/XyZMnVb16dY/kRX5NmjRRgwYNXD7+8vMtSS1btvSpTuNXxfb06dNKSkoq8BuwRo0aOnDgQKmPh/uVxTlZv369atWqpdjYWHdExFWU5BympKRowoQJeuWVVzwREVdR3HOYkpKiAwcOKDs7W4MHD1ZsbKx69uypL7/80lORUYiS/F588MEHNXHiRDVt2lQ33HCDevfurQEDBmjgwIGeiIxS2r9/v893mkCzA3hSamqqJBW4rJzD4cjbV5rj4X6lPSfp6emaOnWqZs2apaCgILdkxJWV5BxOnDhRw4cPV82aNfninwUU9xyeO3dOkvM8rl69WjExMVq1alVeue3evbvbM6OgkvxenD17tqZMmaK4uDhdd9112r59u1auXCm73a/GybxWamqqz3cav/qVGBYWJqngP0Gnp6fn7SvN8XC/0p6TYcOG6Z577lH//v3dkg9XV9xzGB8fr40bN2r48OEeyYerK+45DAgIkCT169dPMTExkqSuXbuqS5cuevXVV92cFkUp7nk0DENPPfWUhg0bpuuuu06SFBMTo2XLlunFF190f2CUWlhYmM93Gr8qtpUrV1ZkZKSOHz+eb/uxY8dUv379Uh8P9yvNORk3bpzCwsI0adIkd0bEVRT3HC5dulQXLlxQly5d1KlTJw0aNEiSNGrUKHXq1En79u3zSG5cVNxzWLVqVTkcDl1zzTX5ttetW9en/gnU2xT3PJ48eVJnz55VVFRUvu316tXT559/7s6oKCP169f3+U7jV8VWkrp06aK4uLi854ZhKD4+Xt26dSuT4+F+JTknU6ZMUUJCgmbNmiVJiouLy/ce8KzinMOJEycqPj5ea9as0Zo1a7RgwQJJ0owZM7RmzZpifXECZac45zAgIECxsbE6evRovu3Hjx/Xtdde6/asKFpxzmOVKlXkcDgKnMejR4/61IifL+vatWuBP/u2bNniW53GxBUZTLFx40YjIiLC2Lt3r2EYhvHhhx/mW7MvNjbWeOaZZ1w+Hp5X3HP45ptvGo0bNzbWr19vbN682di8ebPx3HPPGXPmzDEjPozin8NLHThwgOW+LKC453D58uVGxYoVjUOHDhmGYRi7d+82HA6HsXjxYs+HR57insdHHnnEaNSokXHmzBnDMAwjLi7OCAoKMmbMmOH58MhT1HJf9957rzFkyJC857nr2H733XeGYRjG2rVrfW4dW7/68pgktWrVSnPnztWgQYMUGhoqu92u5cuXKzw8XJJzYvWl80+udjw8rzjnMDk5WY899phycnLUtm3bfO8zZ84cj2eHU3F/H+YaNWqUNmzYkPf4+uuvzxvBhWcV9xz26NFDM2fO1O23367y5csrKytL77//vvr27WvWjwAV/zy+8sor+vvf/66uXbsqLCxMycnJmjJlikaMGGHWj+DXMjIy1KNHj7wvaA4aNEh16tTJW+YyLS0t3xf76tatqyVLlmjs2LEKDg5Wenq6lixZorp165oR3y1shmEYZocAAAAASsvv5tgCAADAN1FsAQAA4BMotgAAAPAJFFsAAAD4BIotAAAAfALFFgAAAD6BYgsAAACfQLEFAACAT6DYAgAAwCdQbAHAR7z22muqW7euAgMD9eSTT5odBwA8jkvqAoAP2L59u1q0aKFFixbp5ptvVmRkpMLCwsyOBQAeFWh2AABA6S1ZskStWrVS7969zY4CAKah2AKAl2vQoIF+/fVXSZLNZtP999+vDz74wORUAOB5TEUAAC934sQJtW3bVo8++qiGDBmi8uXLq3z58mbHAgCP48tjAODlypcvr4MHD+rWW29VjRo1dP/996tixYoaMGCA2dEAwKMotgDg5Xbs2CFJuummmyRJI0eOZCoCAL9EsQUAL7dt2zY1aNBA5cqVkyR16tRJ4eHhJqcCAM+j2AKAl9u2bZtiYmLMjgEApqPYAoCX27Ztm5o1a2Z2DAAwHcUWALxYTk6Odu7cyYgtAIh1bAHAq9ntdqWkpJgdAwAsgXVsAcDHdOvWTdu3b1dKSooqVaqkzz77TG3btjU7FgC4HcUWAAAAPoE5tgAAAPAJFFsAAAD4BIotAAAAfALFFgAAAD6BYgsAAACfQLEFAACAT6DYAgAAwCdQbAEAAOATKLYAAADwCRRbAAAA+ASKLQAAAHzC/wfVrv118D3CHgAAAABJRU5ErkJggg==", + "image/png": "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WZ9//rkeeeQRRUdHq2bNmnl6XyuvzF4qPj5es2fP1i+//CLDMFSqVCkNGzZM4eHhZpcGAABwQyyxMpuYmKgff/xRt99+e57nfPTRR3r00UcdFxMYOHCgqlSpok8++aSAqnRdwcHBGjRokCZNmqQyZcroxIkTevHFF7VgwQLzdzwAAABwAlPDbEREhEqUKJHn848fP64DBw6ofv36WcYbNmyodevW5XN11lG9enW99957atmypdLS0jR9+nS99tprOn36tNmlAQAAFCjTe2avRez/vk1frFixLOPFihXTyZMnc5yXnJys+Pj4LLfCJjAwUMOGDdMTTzwhHx8fbdmyRU899ZRWrVpldmkAAAAFxlJh1tvbW5KUlpaWZTwtLc1xLDvjx49XSEiI4xYWFlagdZrFZrOpS5cuev/991WtWjWdO3dO48eP13vvvacLFy6YXR4AAEC+s1SYDQsLk6enpw4fPpxl/NChQ6pSpUqO84YPH664uDjHLSYmpqBLNVWFChU0ceJE9e7dWzabTb/99psGDx7s2M4LAACgsHD5MHvgwAFt3bpVkuTv76+2bdvq119/dRw/d+6cVq1apc6dO+f4Gr6+vgoODs5yK+y8vLw0YMAAjR8/XqVKldKJEyc0bNgwzZs3jy+HAQCAQsPUMJuSkqINGzZo7969kqRNmzZdsXp47733ql69ekpPT5ckjR07Vt9//71GjhypJUuWqHfv3ipbtqwefvhhp9dvBbVr19YHH3yg2267TYZh6IsvvtDYsWOVkJBgdmkAAAA3zNR9Zo8ePXrFtlw1atTQ7NmzHc+ffvppRUVFafny5Y6xVatW6d1339XRo0dVv359vfrqqypdunSe37ew7DN7rZYsWaKPP/5YaWlpKlWqlF544QXdfPPNZpcFAABwhbzmNZe4nK2zuWuYlaR///1Xb731lo4ePSoPDw/df//96tWrV54vIwwAAOAMlrhoApwvc0/atm3bKiMjQ59//rlGjRqluLg4s0sDAAC4ZoRZNxQQEKChQ4dq8ODB8vHx0caNG/X00087vmgHAABgFYRZN2Wz2dSxY0dNmjRJYWFhOn36tEaMGKE5c+YoIyPD7PIAAADyhDDr5ipVqqRJkyapffv2MgxDX331lV555RUuhQsAACyBMAv5+fnpmWee0fPPPy8/Pz9t3bpVgwcP1vr166/vBRMT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TqwcAK5d+fLlNXLkSMcVwkaMGEHLAQDcIFPD7PTp0/Xwww8rNDRUktStWzfVqVNHM2bMyHFOQkKCqlevLg8Pe+kVKlRQQECAzp0755SaAeBGhIeHa9y4cQoODta///6roUOH6sCBA2aXBQCWZVqYPXLkiI4fP666detmGa9bt642bdqU47wxY8Zo6dKleuONNzR37lz17dtXtWrVclw+MjvJycmKj4/PcgMAs4SHh+vNN99U6dKldezYMQ0dOlSrV682uywAsCTTwuzp06clSUFBQVnGQ0JCdObMmRzn1a5dW926ddPcuXM1Y8YMbdu2TX369LnidS41fvx4hYSEOG5hYWH580MAwHWqUKGC3n33XUVERCgpKUnjxo3Txx9/XPj7/xcskEJCJJvt4q1iRWn/frMrA2BRpoVZPz8/SVJKSkqW8eTkZPn6+mY7xzAMderUSfHx8YqOjtbSpUv1xx9/aMKECXrjjTdyfK/hw4crLi7OcYuJicm/HwQArlNQUJBGjx6t//73v5Kkn3/+Wc8884x27NhhcmUFZMIE6Y47pPh4ydtbCg2VPDykmBipShUpOtrsCgFYkGlhtmLFivL29tbBgwezjB84cEDVq1fPdk5MTIw2b96se+65x9EzW7JkSd16661asGBBju/l6+ur4ODgLDcAcAWenp56+OGHNXr0aIWGhurw4cN64YUX9Pbbb+v48eNml5d/0tKk4cPtj2fPllJSpNhYKT1dGjDAPt60qXn1AbAs08Ksj4+POnfurIULFzrGTp48qVWrVqlHjx6OsQ0bNmjx4sWSpOLFi8tms2nPnj1ZXmvv3r0qUaKEcwoHgALQoEEDffjhh+rQoYNsNpv++usvPf7445o+fXrh6PPPDKzt20v9+mU99vnn9taD5GQpl91sACA7NsMwDLPefNu2bWrRooXuvvtutWjRQlOnTpWXl5dWrFghb29vSVKrVq20atUqpaWlydPTU08++aRmzZqll156SVWqVNHixYs1e/ZsLVq0SJ06dcrT+8bHxyskJERxcXGs0gJwOXv37tXMmTMVFRUlSfL391evXr3UtWtX6/6ZVaaMdPy4lJgo/a/NLIshQ6RJk+xBl60WASjvec3Urblq166tjRs3KigoSEuXLlXv3r3122+/OYKsJHXp0kX9+vWTp6enJOnDDz/U559/riNHjujHH39U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Hz5cx/z8pIcekpo0Mbu8Qo0wCwAAkA9KlCihCRMmqHz58jpx4oSGDRum/fv3m11WoUeYBQAAyCehoaEaN26cwsLCdOrUKb344ovavHmz2WUVaoRZAACAfFS8eHG9+eabqlOnjhITEzVy5EgtWrTI7LIKLcIsAABAPgsKCtLo0aP1n//8RxkZGZo6daqmTp2qtLQ0s0srdAizAAAABcDb21vPPfecBgwYIJvNpkWLFunVV19VXFyc2aUVKoRZAACAAmKz2dS7d2+NGDFC/v7++vvvv/Xss89q9+7dZpdWaBBmAQAAClizZs00adIklS9fXrGxsXrppZe0dOlSs8sqFAizAAAATlChQgW98847atasmVJTUzV58mR98MEHSklJMbs0SyPMAgAAOElgYKBGjBih/v37y2az6ddff9WQIUN0+PBhs0uzLMIsAACAE9lsNt19990aPXq0QkJCtH//fj377LP6448/zC7NkgizAAAAJqhfv74mT56siIgIJSUladKkSZo8ebKSk5PNLs1SCLMAAAAmKV68uMaMGaO+ffvKZrNp6dKlGjJkiGJiYswuzTIIswAAACby8PBQv3799MYbb6ho0aI6cOCAnn32WS1dulSGYZhdnssjzAIAALiAiIgITZ48WfXr11dKSoomT56siRMn6sKFC2aX5tIIswAAAC6iWLFiGj16tAYMGCAPDw/99ddfeuaZZ7jIQi4IswAAAC4k86phb731lkqVKqVjx47pxRdf1IIFC2g7yAZhFgAAwAXVqFFDkydPVsuWLZWWlqbp06dr7NixOnfunNmluRTCLAAAgIsKDAzUsGHDNGjQIHl5eWnt2rV6+umn9c8//5hdmssgzAIAALgwm82mrl276p133lG5cuUUGxurYcOGaf78+bQdiDALAABgCVWrVtV7772ndu3aKSMjQzNnztSECRPcfrcDwiwAAIBF+Pv7a8iQIY62g1WrVun555/XwYMH8/4i48ZJLVpIgwcXXKFOZDPccH06Pj5eISEhiouLU3BwsNnlAAAAXLOdO3dqwoQJio2Nla+vr5555hm1adMm5wldu0q//HLleNWq0p49BVfodcprXmNlFgAAwIJq1Kih9957T/Xr11dycrLeeustzZgxQ+np6Vee3KnTxSDr7S01bCgFBtqf790rlS7tvMLzGWEWAADAokJCQjRq1Cjd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yk+bNm2u6GnftGmT6tat65zC4dC1a1dFRkbq/PnzjrFFixbp1ltvlZ+fnyTp9OnTioyMdHwnJDg4+Ir9aI8cOSKbzZbl+wtwDenp6Vq5cqUjsHbu3FkXLlxQZGSk45xFixapSJEiatOmjVllFiyTv4BW4OLj443q1asb3bp1MxYuXGg88cQTRmBgoLF161bHOa+//rohyViyZEme58B5+vXrZ4SHhxvffPONMXnyZMPX19eYNm2a4/iKFSsMSY7tuw4dOmTUr1/fGDVqlLFixQrHrVatWmb9CG5t2rRphr+/v/HRRx8Zc+fONapUqWLce++9Wc7x9PQ0mjRpkmUsNTXVWLFihfH0008bISEhxooVK9jn0gRbt241AgMDjWeeecZYsGCB0alTJ6N69erGuXPnHOd06tTJkGTExcUZhmEYq1atMnx9fY3nnnvO+OWXX4yXXnrJ8PX1NVatWmXWj+G2EhMTjdq1axvt27c3FixYYAwZMsTw9fU11q9f7zjn7bffNiQZ3333nWEYhjFv3jxDkjF06FBjyZIlxpQpU4zixYsb9913n1k/hlvbt2+fsWLFCqNu3bpG9+7djRUrVhjHjh1zHN+8ebMhyRg0aJBj7NFHHzUqVapkzJkzx5g6daoRGBhYqLfGsxmGYZgZpp3hxIkTGj9+vKKjoxUWFqbnn38+S//k119/rQ8++EDTpk1TzZo18zQHzpOamqoPPvhAS5YsUUBAgPr3769evXo5ju/Zs0cDBgzQY489pv79+2vChAlXtCZIUkBAgH799Vdnlo7/mT9/vr744gtduHBBHTt21NNPP+3ox5Tsq0c333yzJk2a5BiLi4tTt27dsrzO3XffraefftppdcMuOjpakyZNUkxMjOrWravhw4dnabl67bXXFBkZqV9//VVeXvbutQ0bNuiDDz7QkSNHVL16dQ0ePFi1atUy60dwa6dPn9b48eO1efNmlS1bVs8884waN27sOL5w4UK99dZbmjx5sho2bChJ+uuvv/R///d/OnjwoIoVK6b27dvrgQceyPLfLZzjo48+0pw5c7KMDRs2TN27d5dkXzW/5557dN999+mxxx6TZF+tnTJlihYtWiQfHx/17dtXffr0cXrtzuIWYRYAAACFEw2gAAAAsCzCLAAAACyLMAsAAADLIswCAADAsgizAAAAsCzCLAAAACyLMAsAAADLIswCgEWlp6frzTffVMeOHdWkSRNt3brV7JIAwOm8zC4AAHB93n//fU2bNk1Tp05VsWLFVKNGDbNLAgCn4wpgAGBRjRo1Ut++fTV06FCzSwEA09BmAAAWExUVpcaNGysqKkpTp05V48aNFRkZaXZZAGAKVmYBwGLOnj2rBQsWaODAgVq+fLkCAgJUq1YtBQQEmF0aADgdPbMAYDFFixZVenq6wsLC1LZtW7PLAQBT0WYAABYUFRWl+vXrO57/8ssv6t69u7p3766VK1eaVxgAOBlhFgAsaMuWLapXr57jee3atfX444/r/PnzOnr0qImVAYBzEWYBwIKio6OzrMxWrFhR3bt3V/ny5c0rCgBMQJgFAIs5cOCAzp49m2VlFgDcFWEWACxmy5YtKlKkiKpVq2Z2KQBgOsIsAFhMy5YttWbNGtlsNrNLAQDTsc8sABQCUVFRGjFihLZs2aIyZcqoevXqmjt3rtllAUCBY59ZACgEypcvr8cff9zx3Nvb28RqAMB5WJkFAACAZdEzCwAAAMsizAIAAMCyCLMAAACwLMIsAAAALIswCwAAAMsizAIAAMCyCLMAAACwLMIsAAAALIswCwAAAMsizAIAAMCy/h9HVn9+67mX5wAAAABJRU5ErkJggg==", "text/plain": [ "
" ] @@ -1295,7 +1205,14 @@ { "cell_type": "code", "execution_count": 10, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-05T19:37:42.174366Z", + "iopub.status.busy": "2026-07-05T19:37:42.174120Z", + "iopub.status.idle": "2026-07-05T19:37:42.179075Z", + "shell.execute_reply": "2026-07-05T19:37:42.177721Z" + } + }, "outputs": [], "source": [ "from pymoo.problems.multi import SRN\n", @@ -1314,7 +1231,14 @@ { "cell_type": "code", "execution_count": 11, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-05T19:37:42.184985Z", + "iopub.status.busy": "2026-07-05T19:37:42.184657Z", + "iopub.status.idle": "2026-07-05T19:37:42.188075Z", + "shell.execute_reply": "2026-07-05T19:37:42.187242Z" + } + }, "outputs": [], "source": [ "def calc_cv(X):\n", @@ -1332,7 +1256,14 @@ { "cell_type": "code", "execution_count": 12, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-05T19:37:42.189544Z", + "iopub.status.busy": "2026-07-05T19:37:42.189387Z", + "iopub.status.idle": "2026-07-05T19:37:42.196854Z", + "shell.execute_reply": "2026-07-05T19:37:42.196175Z" + } + }, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", @@ -1381,11 +1312,18 @@ { "cell_type": "code", "execution_count": 13, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-05T19:37:42.199071Z", + "iopub.status.busy": "2026-07-05T19:37:42.198924Z", + "iopub.status.idle": "2026-07-05T19:37:42.324384Z", + "shell.execute_reply": "2026-07-05T19:37:42.323733Z" + } + }, "outputs": [ { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -1411,11 +1349,18 @@ { "cell_type": "code", "execution_count": 14, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-05T19:37:42.326167Z", + "iopub.status.busy": "2026-07-05T19:37:42.326018Z", + "iopub.status.idle": "2026-07-05T19:37:42.466235Z", + "shell.execute_reply": "2026-07-05T19:37:42.465632Z" + } + }, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAiUAAAITCAYAAADYTmxbAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAACKuUlEQVR4nO3dd1iTZ9sG8DNhCgIiCi4cKG4RQdwb9957z9a29q1tv6pd2qG1rd1D2zrqaN171z3qAtwTERBR2XtDnu+Pu4kslZHwZJy/4+BQnsRwRZScucd1KyRJkkBEREQkM6XcBRAREREBDCVERESkJxhKiIiISC8wlBAREZFeYCghIiIivcBQQkRERHqBoYSIiIj0AkMJERER6QWGEiIiItILBhdKNm/ejJ49e8LX1xc+Pj4YMWIEQkJCNLdLkoRPPvkEXl5eaNWqFcaPH4+EhAT5CiYiIqIiMbhQMn78eLz99ts4evQoLly4gHLlyqF3797IyMgAAHz77bfYtm0bzp49i4sXL8LS0hITJkyQuWoiIiJ6GYWhnX0zYsQIbNmyRfO5n58ffHx88O+//6JVq1aoWrUqPv30U8yaNQsAcOvWLTRp0gTXrl1Ds2bN5CqbiIiIXsLgRkpyBxIAsLa2BgBkZGTg2rVriIqKQsuWLTW3N2rUCLa2tjhy5EiZ1klERETFYy53AaV17tw5VKtWDe3bt8fu3bsBAC4uLprbFQoFXFxcEBwcXOifz8jI0Ez9AIBKpUJsbCycnJygUCh0WzwREZGBkyQJSUlJqFatGpTK0o11GHQoycjIwFdffYWffvoJFhYWSE1NBQBYWVnluZ+VlZXmtvyWLFmCRYsW6bxWIiIiYxYWFoYaNWqU6jEMOpTMmjULo0aNwpAhQwAANjY2AJBn5EP9ufq2/ObPn4+5c+dqPk9ISEDNmjURFhYGe3t7HVVORERkHBITE+Hq6go7O7tSP5bBhpJ58+bBxsYGn376qeaam5sbACAiIiJPWouIiNDclp+VlVWBkRUAsLe3ZyghIiIqIm0seTDIUPLFF18gLCwM69atAwD4+/sDADw9PVG5cmX4+/vD29sbAHD79m2kpKSge/fustVLREREL2dwu2+WL1+O9evX44033kBAQAD8/PywZ88eXL9+HWZmZpg3bx5++eUXpKWlAQCWLVuGAQMGoGnTpjJXTkRERC9iUCMlSUlJeO2116BSqdC2bds8t61evRoA8NZbbyE5ORnt27eHubk53N3dsXbtWjnKJSIiomIwuOZpupaYmAgHBwckJCRwTQkREdFLaPN10+Cmb4iIiMg4MZQQERGRXmAoISIiIr3AUEJERER6gaGEiIiI9AJDCREREekFhhIiIiLSCwwlREREpBcYSoiIiEgvMJQQERGRXmAoISIiIr3AUEJERER6gaGEiIiI9AJDCREREekFhhIiIiLSCwwlREREpBcYSoiIiEgvMJQQERGRXmAoISIiIr3AUEJERER6gaGEiIiI9AJDCREREekFhhIiIiLSCwwlREREpBcYSoiIiEgvMJQQERGRXmAoISIiIr3AUEJERER6gaGEiIiI9AJDCREREekFhhIiIiLSCwwlREREpBcYSoiIiEgvMJQQERGRXmAoISIiIr3AUEJERER6gaGEiIiI9AJDCREREekFhhIiIiLSCwwlREREpBcYSoiIiEgvMJQQERGRXmAoISIiIr3AUEJERER6gaGEiIiI9AJDCREREekFc7kLKK7MzEx89NFH+Prrr3H//n3Url1bc9vkyZNx584dWFtba641btwYv/zyiwyVEhERUXEYVCgJCQnBmDFjUL9+feTk5BR6n40bN+YJKkRERGQYDGr6Jjk5GevWrcOUKVPkLoWIiIi0zKBGSpo2bQoAePTokcyVEBERkbYZVCgpiiVLluDu3bvIzs5G8+bN8dFHH8HFxeW598/IyEBGRobm88TExLIok8ikJCVl4/bteKhUsVAo4pCUlIT09HTN/z/1h/pa7tuys7OhVCphZmYGpVKZ5/f5r1lYWKB8+fKws7ND+fLl83yor9nZ2cHCwkLuvxIiKoRRhZL69eujVq1a+PXXX5GTk4NXX30Vbdq0wfXr11G+fPlC/8ySJUuwaNGiMq6UyLgkJiYiIiICcXFxmo/Y2FhERMRj795YXL+ehKwscd/q1YFWrYDKleWr19LSEg4ODnB2di7w4eTkBDMzM/mKIzJhCkmSJLmLKK4TJ06ga9euCA4OfuGi1sTERDg6OmL58uWYMWNGofcpbKTE1dUVCQkJsLe313bpRAYtIyMDT548QXh4eJ6PpKSkAvfNyQH27gViYoBmzYCaNS2Qk+MIPz9HxMba4/33rdGokRWsra1hZfXs1/y/Nzc3h0qlgkqlQk5OTp5f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", + "image/png": "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", "text/plain": [ "
" ] @@ -1441,11 +1386,18 @@ { "cell_type": "code", "execution_count": 15, - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-05T19:37:42.467993Z", + "iopub.status.busy": "2026-07-05T19:37:42.467825Z", + "iopub.status.idle": "2026-07-05T19:37:47.219375Z", + "shell.execute_reply": "2026-07-05T19:37:47.218678Z" + } + }, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAiUAAAITCAYAAADYTmxbAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8g+/7EAAAACXBIWXMAAA9hAAAPYQGoP6dpAACKSElEQVR4nO3dd1iTZ9sG8DNhbxEEFw4cuGW4caCodVute9Zql9ba2vat2qWtVavd1rbOOj+17m3de1RAxS0yFBGRvXee74+7iaCogAlPEs7fcXAoSSBXDJIz97huhSRJEoiIiIhkppS7ACIiIiKAoYSIiIj0BEMJERER6QWGEiIiItILDCVERESkFxhKiIiISC8wlBAREZFeYCghIiIivcBQQkRERHrB4ELJ33//je7du8Pf3x8tW7bE4MGDERERoblekiR8/fXX8Pb2RqtWrTBq1CgkJyfLVzAREREVi8GFklGjRuGjjz7C4cOHcf78eVhZWaFHjx7Izs4GAPz000/YsmULTp8+jX///Rfm5uYYPXq0zFUTERHRiygM7eybwYMHY9OmTZrPAwIC0LJlS5w5cwatWrVClSpV8M033+Dtt98GAFy/fh2NGzdGcHAwmjZtKlfZRERE9AIGN1JSMJAAgKWlJQAgOzsbwcHBiI2NRYsWLTTXN2zYEDY2Njh06FCZ1klEREQlYyp3AS/r7NmzqFq1Knx9fbFz504AgKurq+Z6hUIBV1dXhIeHF/n12dnZmqkfAFCpVEhISICTkxMUCoVuiyciIjJwkiQhNTUVVatWhVL5cmMdBh1KsrOzsWDBAvz2228wMzNDRkYGAMDCwqLQ7SwsLDTXPWnu3LmYNWuWzmslIiIyZpGRkahevfpLfQ+DDiVvv/02hg4digEDBgAArK2tAaDQyIf6c/V1T5o+fTqmTp2q+Tw5ORk1atRAZGQk7O3tdVQ5ERGRcUhJSYGbmxvs7Oxe+nsZbCiZNm0arK2t8c0332guc3d3BwDExMQUSmsxMTGa655kYWHx1MgKANjb2zOUEBERFZM2ljwYZCiZN28eIiMjsWbNGgBAYGAgAMDT0xOVKlVCYGAgfHx8AAA3btxAeno6unbtKlu9RERE9GIGt/vmzz//xNq1azF58mQEBQUhICAAu3btwpUrV2BiYoJp06bh999/R2ZmJgDghx9+QN++fdGkSROZKyciIqLnMaiRktTUVEyaNAkqlQpt27YtdN1ff/0FAPjwww+RlpYGX19fmJqaol69eli9erUc5RIREVEJGFzzNF1LSUmBg4MDkpOTuaaEiIjoBbT5umlw0zdERERknBhKiIiISC8wlBAREZFeYCghIiIivcBQQkRERHqBoYSIiIj0AkMJERER6QWGEiIiItILDCVERESkFxhKiIiISC8wlBAREZFeYCghIiIivcBQQkRERHqBoYSIiIj0AkMJERER6QWGEiIiItILDCVERESkFxhKiIiISC8wlBAREZFeYCghIiIivcBQQkRERHqBoYSIiIj0AkMJERER6QWGEiIiItILDCVERESkFxhKiIiISC8wlBAREZFeYCghIiIivcBQQkRERHqBoYSIiIj0AkMJERER6QWGEiIiItILDCVERESkFxhKiIiISC8wlBAREZFeYCghIiIivcBQQkRERHqBoYSIiIj0AkMJERER6QWGEiIiItILDCVERESkFxhKiIiISC8wlBAREZFeYCghIiIivcBQQkRERHqBoYSIiIj0gqncBZRUTk4OvvzyS3z//fe4c+cOatWqpbnu9ddfx82bN2Fpaam5rFGjRvj9999lqJSIiIhKwqBCSUREBIYPH4769esjPz+/yNts2LChUFAhIv118SIwfz6wZw+Qlwd07Ah8/DHQtavclRGRHAwqlKSlpWHNmjW4f/8+Vq9eLXc5RPQSDh4E+vYF3NyATz4BrKyAjRuBbt2AP/8E3n5bd/cdHQ2sXg2EhwNVqgCjRwPu7rq7PyIqHoNaU9KkSRPUrVtX7jKI6CXl5QHjxgGdOgFXrwJffCFGSP79F3j3XeD994GYGN3c97JlQM2awNdfAxcuAD//DNStKz4nInkZ1EhJccydOxe3bt1CXl4emjdvji+//BKurq7PvH12djays7M1n6ekpJRFmUTlSl5eHpKSkpCQkIDExEQcOpSKqKgsfPBBNrZsydb8P8zKyoKLSzZUqiyMHJmN5s2zkZeXB6VSCRMTEyiVykJ/f/IyMzMz2Nraws7ODra2toU+7OzsEBxsizfftMPbb5vhu+8ABwcgI0NMIX31FVCvHjB8uNz/WkTll1GFkvr166NmzZr4448/kJ+fj3fffRdt2rTBlStXYGtrW+TXzJ07F7NmzSrjSon019WrwPffA/v3A/n5YjTjo4+Atm2f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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAiYAAAISCAYAAAD4JQT9AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAiTVJREFUeJzt3Xdc1dX/B/DXZS8FBGUIoiLuyRDCrblHztwrM7VlWpnaMLM0Ta2+1c/UHFlaWmq4d+4NgoooKiq4QLYyLuN+fn+cuILgAO69n3svr+fjwaP43Cv3DVe5r3vG+ygkSZJAREREpAdM5C6AiIiIqACDCREREekNBhMiIiLSGwwmREREpDcYTIiIiEhvMJgQERGR3mAwISIiIr3BYEJERER6g8GEiIiI9IZBBZPt27djyJAh6NChA8aOHYuTJ08Wu09eXh4WLVqEl19+Gb1798aGDRtkqJSIiIjKwmCCyVdffYVRo0ahY8eO+OSTT2BnZ4fg4GDs2rWryP3GjRuHn3/+GZMmTULPnj0xevRoLFmyRKaqiYiIqDQUhnJWzvTp01G9enW888476muBgYGoWrUqtm3bBgCIiIhA8+bNcerUKbRs2RIAsGDBAsybNw/379+HpaWlLLUTERHRizGYEZO33noLo0ePLnKtTp06SE9PV3++Y8cOODo6qkMJAPTo0QOpqak4ceKEzmolIiKisjGTu4AX5enpWexaeHg4Jk6cqP78xo0b8PDwKHKfgs9jYmLQvn37Yl9DqVRCqVSqP1epVEhOToaTkxMUCoWGqiciIjJOkiTh4cOHcHd3h4lJ+cc7DCaYPGn9+vWws7PDpEmT1Ndyc3NhZlb0WzI3N1ffVpJ58+Zh9uzZ2iuUiIioAoiLiys2OFAWBhlMTp06hXnz5mH37t1FgoizszNSU1OL3Lfgc2dn5xK/1owZMzB16lT152lpaahRowbi4uJQuXJljddORERkTNLT0+Hp6YlKlSpp5OsZXDDZuXMn5s+fj507d8LFxaXIbQEBAVi8eDFSU1Ph4OAAADh37pz6tpJYWlqWuCi2cuXKDCZEREQvSFPLHwxm8SsALFu2DMuXL8f27dvh5uYGABg7dqz69t69e8PNzU09NZOdnY25c+eiR48eqFGjhiw1ExER0YszmO3CP/zwAyZPngx/f/8ii2uio6ORnJys/vzMmTMYOHCgejFOvXr1EBISUmx05WnS09Nhb2+PtLQ0jpgQERE9h6ZfNw1mKqdnz54lTsc8uQI4ICAAMTExiIqKgpWVFerUqaOrEomIiKicDCaY1K5dG7Vr136h+5qamqJx48ZaroiIiIg0zaDWmBAREZFxYzAhIiIivcFgQkRERHqDwYSIiIj0BoMJERER6Q0GEyIiItIbDCZERESkNxhMiIiISG8wmBAREZHeYDAhIiIivcFgQkRERHqDwYSIiIj0BoMJERER6Q0GEyIiItIbDCZERESkNxhMiIiISG8wmBAREZHeYDAhIiIivcFgQkRERHqDwYSIiIj0BoMJERER6Q0GEyIiItIbDCZERESkNxhMiIiISG8wmBAREZHeYDAhIiIivcFgQkRERHqDwYSIiIj0BoMJERER6Q0GEyIiItIbDCZERESkNxhMiIiISG8wmBAREZHeYDAhIiIivcFgQkRERHqDwYSIiIj0BoMJERER6Q0GEyIiItIbDCZERESkNxhMiIiISG8wmBAREZHeYDAhIiIivcFgQkRERHqDwYSIiIj0BoMJERER6Q0zuQsoi/z8fEiSBDOz4uUrlUpIklTkmrm5OUxNTXVVHhEREZWRwY2YREZGokGDBnjvvfdKvN3T0xMODg5FPr755hvdFklERERlYlAjJps2bcK8efOQkZHx1Pv4+vpi165dOqyKiOR0/Trw44/AwYOAiQnQtSvw5puAh4fclRFRWRjUiElCQgKOHj0KNze35973yekcIjI++/cDTZsCv/0G+PuL///pJ/Hfs2flro6IysKgRkwmTpz43PskJyeja9euOHToEKpUqYJ+/fph3rx5qFy5sg4qJDJu164BP/8MhIUBtrbAwIHA4MGAlVX5vq4kScjJyYFSqUR2djaUSqX6/3Nzc2FiYqL+MDU1hYmJCXJyTDFwoAn8/EywYoUp7OxMYWpqii+/tEX//jYYPFiB6GiAy8uIDItBBZMXkZmZiTlz5mDLli24evUqXn31VYwePRqbN28u8f4FvwALpKen66pUIoOyYQMwYgRQqRLQqROQkACMGQN8/z2wbx9QpUrR++fk5CAlJQUpKSlITk5GamoqkpOTkZKSgrS0tGIhpLSio4HUVKB6dWDhwqK3eXiY4PRpW4waZQdf30qws7Mr9lG5cmVUrVoVVapUgYmJQQ0eExk1owsmFy9eVP9/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", "text/plain": [ "
" ] @@ -1503,7 +1455,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.5" + "version": "3.11.15" }, "pycharm": { "stem_cell": { diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bb3392e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,35 @@ +[tool.pytest.ini_options] +testpaths = ["tests"] +markers = [ + "slow: long-running tests (deselected by the default test tier)", + "integration: cross-component integration tests", + "golden: behavior-regression baseline tests", +] + +[tool.ruff] +# Default scope for `ruff check` / `ruff format` when no path is given. +src = ["src"] +include = ["src/**/*.py", "tests/**/*.py"] +extend-exclude = [ + "src/pysamoo/vendor", + "src/pysamoo/usage", + "src/pysamoo/experimental", + "tests/_golden_plugin.py", # vendored, generated by `pyclawd golden vendor` +] +line-length = 120 +target-version = "py311" + +[tool.ruff.lint] +# Conservative default rule set for an existing codebase. +select = ["E", "F", "W", "I"] +# E501: long lines; E741: `I`/`l`/`O` are idiomatic index/array names in this +# numpy-heavy optimization code. +ignore = ["E501", "E741"] + +[tool.mypy] +files = ["src"] +ignore_missing_imports = true +follow_imports = "silent" +# Legacy signatures use `x: T = None`; keep the pre-PEP-484 implicit-Optional rule. +implicit_optional = true +exclude = ["src/pysamoo/vendor", "src/pysamoo/usage", "src/pysamoo/experimental"] diff --git a/pysamoo/__init__.py b/pysamoo/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pysamoo/algorithms/__init__.py b/pysamoo/algorithms/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pysamoo/algorithms/ssansga2.py b/pysamoo/algorithms/ssansga2.py deleted file mode 100644 index 7379a86..0000000 --- a/pysamoo/algorithms/ssansga2.py +++ /dev/null @@ -1,95 +0,0 @@ -from pymoo.algorithms.moo.nsga2 import NSGA2 -from pymoo.core.duplicate import DefaultDuplicateElimination -from pymoo.core.population import Population -from pymoo.optimize import minimize -from pymoo.util.display.multi import MultiObjectiveOutput -# from pymoo.util.output import MultiObjectiveOutput -from pymoo.util.nds.non_dominated_sorting import NonDominatedSorting -from pymoo.util.normalization import normalize -from pymoo.util.roulette import RouletteWheelSelection -from sklearn.cluster import KMeans - -from pysamoo.core.algorithm import SurrogateAssistedAlgorithm - - -class SSANSGA2(SurrogateAssistedAlgorithm): - - def __init__(self, - n_infills=10, - surr_pop_size=100, - surr_n_gen=30, - surr_eps_elim=1e-6, - surr_sampling="current", - output=MultiObjectiveOutput(), - **kwargs): - - super().__init__(output=output, **kwargs) - self.n_infills = n_infills - self.surr_n_gen = surr_n_gen - self.surr_pop_size = surr_pop_size - self.surr_eps_elim = surr_eps_elim - self.surr_sampling = surr_sampling - - def _initialize_advance(self, infills=None, **kwargs): - super()._initialize_advance(infills, **kwargs) - self.surrogate.validate(infills) - - def _infill(self): - - self.surrogate.fit(self._archive) - - problem = self.surrogate.problem() - - if self.surr_sampling == "current": - sampling = self._archive - elif self.surr_sampling == "random": - sampling = None - else: - raise Exception("Unknown surrogate sampling strategy.") - - algorithm = NSGA2(pop_size=self.surr_pop_size, - sampling=sampling - ) - - res = minimize(problem, - algorithm, - ('n_gen', self.surr_n_gen), - seed=1, - verbose=False) - - cand = DefaultDuplicateElimination(epsilon=self.surr_eps_elim).do(res.pop, self._archive) - - if len(cand) <= self.n_infills: - infills = Population.new(X=cand.get("X")) - - else: - - ideal = res.opt.get("F").min(axis=0) - nadir = res.opt.get("F").max(axis=0) + 1e-16 - vals = normalize(cand.get("F"), ideal, nadir) - - kmeans = KMeans(n_clusters=self.n_infills, random_state=0).fit(vals) - groups = [[] for _ in range(self.n_infills)] - for k, i in enumerate(kmeans.labels_): - groups[i].append(k) - - S = [] - - for group in groups: - if len(group) > 0: - fitness = cand[group].get("crowding").argsort() - selection = RouletteWheelSelection(fitness, larger_is_better=False) - I = group[selection.next()] - S.append(I) - - infills = Population.new(X=cand[S].get("X")) - - return infills - - def _advance(self, infills=None, **kwargs): - self.surrogate.validate(self._archive, infills) - super()._advance(infills, **kwargs) - - def _set_optimum(self): - nds = NonDominatedSorting().do(self._archive.get("F"), only_non_dominated_front=True) - self.opt = self._archive[nds] diff --git a/pysamoo/core/__init__.py b/pysamoo/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pysamoo/core/algorithm.py b/pysamoo/core/algorithm.py deleted file mode 100644 index 7d77631..0000000 --- a/pysamoo/core/algorithm.py +++ /dev/null @@ -1,111 +0,0 @@ -from pymoo.core.algorithm import Algorithm -from pymoo.core.initialization import Initialization -from pymoo.core.population import Population -from pymoo.operators.sampling.lhs import LHS -from pymoo.util.normalization import ZeroToOneNormalization - -from pysamoo.core.defaults import DEFAULT_OBJ_MODELS, DEFAULT_IEQ_CONSTR_MODELS, DEFAULT_EQ_CONSTR_MODELS -from pysamoo.core.surrogate import Surrogate -from pysamoo.core.target import Target - - -def default_n_doe(n, max=float("inf")): - return min(2 * n + 1, max) - - -class SurrogateAssistedAlgorithm(Algorithm): - - def __init__(self, - n_initial_doe=None, - n_initial_max_doe=100, - sampling=LHS(), - nth_validate=5, - surrogate=None, - **kwargs): - """ - Parameters - ---------- - n_initial_doe : int - Number of initial design of experiments. If `None`, the default is 11*n - 1. (but at most `n_max_doe`) - - n_max_doe : int - If `n_initial_doe` is set to `None`, the maximum number of initial designs. - - sampling : class - The initial sampling being used for the designs of experiment. - - """ - super().__init__(**kwargs) - - self.n_initial_doe = n_initial_doe - self.n_initial_max_doe = n_initial_max_doe - self.initialization = Initialization(sampling) - - # all solutions that have been evaluated so far - self._archive = Population() - - # here always the most recent infill solutions are stored - self.infills = None - - # the model/surrogate to be used during optimization - self.surrogate = surrogate - - # a solution set which has not been evaluated yet on the models - self.validation = Population() - - # each nth iteration when all surrogate models should be revalidated - self.nth_validate = nth_validate - - def _setup(self, problem, **kwargs): - - # initialize the default surrogate for the algorithm - if self.surrogate is None: - - # the design space boundaries for the problem - used for normalization in the surrogate - xl, xu = problem.bounds() - defaults = dict(norm_X=MyNormalization(xl, xu)) - - targets = [] - - models = DEFAULT_OBJ_MODELS(**defaults) - for m in range(problem.n_obj): - target = Target(("F", m), models) - targets.append(target) - - models = DEFAULT_IEQ_CONSTR_MODELS(**defaults) - for g in range(problem.n_ieq_constr): - target = Target(("G", g), models) - targets.append(target) - - models = DEFAULT_EQ_CONSTR_MODELS(**defaults) - for h in range(problem.n_eq_constr): - target = Target(("H", h), models) - targets.append(target) - - # create the surrogate model - self.surrogate = Surrogate(problem, targets) - - # set the number of DOE points initially - if self.n_initial_doe is None: - self.n_initial_doe = min(self.n_initial_max_doe, default_n_doe(problem.n_var)) - - def _initialize_infill(self): - self.infills = self.initialization.do(self.problem, self.n_initial_doe, algorithm=self) - return self.infills - - def _initialize_advance(self, infills=None, **kwargs): - self.infills = infills - self._archive = Population.merge(self._archive, infills) - - def _advance(self, infills=None, **kwargs): - self.infills = infills - self._archive = Population.merge(self._archive, infills) - - -class MyNormalization(ZeroToOneNormalization): - - def forward(self, X): - return super().forward(X) * 200 - 100 - - def backward(self, X): - return super().backward((X + 100) / 200) diff --git a/pysamoo/core/archive.py b/pysamoo/core/archive.py deleted file mode 100644 index bc18f6a..0000000 --- a/pysamoo/core/archive.py +++ /dev/null @@ -1,22 +0,0 @@ -from pymoo.core.population import Population - - -class Archive: - - def __init__(self, survival, max_size=100, trunc_size=None, problem=None) -> None: - super().__init__() - self.sols = Population() - self.survival = survival - self.max_size = max_size - self.trunc_size = trunc_size if trunc_size is not None else max_size - self.problem = problem - - def add(self, sols): - - sols = Population.merge(self.sols, sols) - - if len(sols) > self.max_size: - sols = self.survival.do(self.problem, sols, n_survive=self.trunc_size) - - self.sols = sols - diff --git a/pysamoo/core/defaults.py b/pysamoo/core/defaults.py deleted file mode 100644 index f0132c0..0000000 --- a/pysamoo/core/defaults.py +++ /dev/null @@ -1,94 +0,0 @@ -from ezmodel.core.factory import models_from_clazzes -from ezmodel.models.kriging import Kriging -from ezmodel.models.rbf import RBF -from ezmodel.util.transformation.plog import Plog -from pymoo.util.normalization import NoNormalization - - -def DEFAULT_OBJ_MODELS(**defaults): - - models = models_from_clazzes( - # YAGP, - # BOTORCH, - # TKGP, - # Kriging, - # GGP, - # LGP, - RBF, - # KNN, - # RBF3, - # PolynomialRegression, - # RBF2, - # pySOTRBF, - # SVR, - # InverseDistanceWeighting, - # NearestNeighbors, - **defaults) - - models = {name: entry["model"] for name, entry in models.items()} - - # models = {} - - # for kernel in ["cubic", "linear", "mq"]: - # for normalized in [False, True]: - # for tail in ["constant", "linear", "linear+quadratic"]: - # params = dict(defaults) - # params["kernel"] = kernel - # params["normalized"] = normalized - # params["tail"] = tail - # - # model = RBF(**params) - # models[f"rbf-{kernel}-{tail}-{normalized}"] = model - - models['kriging-const'] = Kriging(regr="constant") - models['kriging-lin'] = Kriging(regr="linear") - models['kriging-quadr'] = Kriging(regr="quadratic") - - models['kriging-const-ARD'] = Kriging(regr="constant", ARD=True) - models['kriging-lin-ARD'] = Kriging(regr="linear", ARD=True) - models['kriging-quadr-ARD'] = Kriging(regr="quadratic", ARD=True) - # models['kriging-sine'] = Kriging(regr="sine") - - return models - - -def DEFAULT_IEQ_CONSTR_MODELS(**defaults): - models = {} - - for kernel in ["cubic", "linear", "mq"]: - for label, norm in [("default", NoNormalization()), ("plog", Plog())]: - for normalized in [False, True]: - for tail in ["constant", "linear", "linear+quadratic"]: - params = dict(defaults) - params["kernel"] = kernel - params["norm_X"] = norm - params["normalized"] = normalized - params["tail"] = tail - - model = RBF(**params) - models[f"rbf-{kernel}-{tail}-{label}-{normalized}"] = model - - return models - - -def DEFAULT_EQ_CONSTR_MODELS(**defaults): - models = {} - - for kernel in ["cubic", "linear", "mq"]: - for normalized in [False, True]: - for tail in ["constant", "linear", "linear+quadratic"]: - for optimize in [False]: - params = dict(defaults) - params["kernel"] = kernel - params["normalized"] = normalized - params["tail"] = tail - params["optimize"] = optimize - - model = RBF(**params) - models[f"rbf-{kernel}-{tail}-{normalized}-{optimize}"] = model - - models['kriging-const'] = Kriging(regr="constant") - models['kriging-lin'] = Kriging(regr="linear") - models['kriging-quadr'] = Kriging(regr="quadratic") - - return models diff --git a/pysamoo/core/knockout.py b/pysamoo/core/knockout.py deleted file mode 100644 index 9812ff6..0000000 --- a/pysamoo/core/knockout.py +++ /dev/null @@ -1,128 +0,0 @@ -from collections import Counter - -import numpy as np - -from pymoo.core.population import Population -from pymoo.core.replacement import ReplacementSurvival -from pymoo.util.dominator import get_relation - -from pysamoo.core.tcv import TotalConstraintViolation - - -def is_better(a, b): - assert len(a) == len(b) - - ret = np.full(len(a), False) - - a_f, a_cv, a_feas = a.get("f", "cv", "feas") - b_f, b_cv, b_feas = b.get("f", "cv", "feas") - - # 1) Both infeasible and constraints have been improved - ret[(~a_feas & ~b_feas) & (b_cv < a_cv)] = True - - # 2) A solution became feasible - ret[~a_feas & b_feas] = True - - # 3) Both feasible but objective space value has improved - ret[(a_feas & b_feas) & (b_f < a_f)] = True - - return ~ret - - -def comp(a, b, error=None): - if error is not None: - a = noisy(a, error) - b = noisy(b, error) - ret = is_better(a, b) - return ret - - -def pcomp(sols, pairs, error=None): - a, b = pairs.T - a_is_better_than_b = comp(sols[a], sols[b], error=error) - - ret = np.copy(b) - ret[a_is_better_than_b] = a[a_is_better_than_b] - - return ret - - -def knockout(sols, n_winners=1, error=None): - # create a copy of all solutions to be considered - pool = list(np.random.permutation(len(sols))) - - # until we have found a clear winner of the tournament - while len(pool) > n_winners: - - # the list of winners in this round - winners = [] - - # make sure the pool is an even number, otherwise add one randomly (not equal to the other competing index) - if len(pool) % 2 != 0: - winners.append(pool[-1]) - pool = pool[:-1] - - # create the pairs that compete with each other - pairs = np.reshape(pool, (-1, 2)) - - # now add all the winners as well from this tournament - W = pcomp(sols, pairs, error=error) - winners.extend(W) - - # that means we have now less than we want - fill up with random solutions from pool - if len(winners) < n_winners: - S = set(winners) - - for k in np.random.permutation(len(sols)): - - # if not added yet then add it - if k not in S: - winners.append(k) - - # we have filled up to the required number of winners -> done - if len(winners) == n_winners: - break - - pool = winners - - return sols[pool] - - -class NoisyReplacement(ReplacementSurvival): - - def __init__(self, error, **kwargs): - super().__init__(**kwargs) - self.error = error - - def _do(self, problem, pop, off, **kwargs): - return comp(off, pop, error=self.error) - - -def calc_prob_relation(a, b, error=None, n_comparisons=1): - ret = [] - - for k in range(n_comparisons): - if error is not None: - _a, _b = noisy(Population.create(a, b), error) - else: - _a, _b = a, b - - _rel = get_relation(_a, _b) - ret.append(_rel) - - rel = np.random.choice([val for val, freq in Counter(ret).most_common()]) - - return rel - - -def noisy(sols, error): - out = {} - for type in ["X", "F", "G", "H"]: - out[type] = np.copy(sols.get(type)) - - for (type, k), std in error.items(): - out[type][:, k] += np.random.normal(loc=0.0, scale=std, size=len(sols)) - - noisy = Population.new(**out) - TotalConstraintViolation().do(noisy, inplace=True) - return noisy diff --git a/pysamoo/core/surrogate.py b/pysamoo/core/surrogate.py deleted file mode 100644 index 08d238b..0000000 --- a/pysamoo/core/surrogate.py +++ /dev/null @@ -1,80 +0,0 @@ -import numpy as np -from pymoo.core.meta import Meta - -from pymoo.core.problem import Problem - - -class Surrogate: - - def __init__(self, - problem, - targets=None, - **kwargs): - - """ - - This surrogate object allows to conveniently build and update surrogates for a Population object. - This can be a rather complicated task because surrogates for objectives and ieq. and eq. constraints might - need to be build and different combinations of doing that are possible. - - Parameters - ---------- - problem : Problem - The optimization problem to access the meta data (it will never be called for an evaluation) - - targets : list - A list of target objects describing how each of the components of a population should be modeled. - This also includes the type of model and other hyper-parameters. This modular definition is necessary - because different types of surrogate might be used for different _targets. - - """ - - super().__init__(**kwargs) - self._problem = problem - self.targets = targets if targets is not None else [] - - def validate(self, trn=None, tst=None, **kwargs): - for target in self.targets: - target.validate(trn=trn, tst=tst, **kwargs) - - def fit(self, sols): - for target in self.targets: - target.fit(sols) - - def performance(self, indicator, **kwargs): - ret = {} - for target in self.targets: - ret[target.label] = target.performance(indicator, **kwargs) - return ret - - def problem(self): - return ProblemFromTargets(self._problem, self.targets) - - -class ProblemFromTargets(Meta): - - def __init__(self, problem, targets, **kwargs): - super().__init__(problem, **kwargs) - self.targets = targets - - def _evaluate(self, X, out, *args, **kwargs): - n = len(X) - - out["F"] = np.full((n, self.n_obj), np.nan, dtype=float) - out["G"] = np.full((n, self.n_ieq_constr), np.nan, dtype=float) - out["H"] = np.full((n, self.n_eq_constr), np.nan, dtype=float) - - for target in self.targets: - target.predict(X, out) - - for v in ["F", "G", "H"]: - if np.any(np.isnan(out[v])): - raise Exception("Building Surrogate has failed (nan values were predict). The run has been terminated.") - - out["F_estm_error"] = np.full((n, self.n_obj), np.nan, dtype=float) - out["G_estm_error"] = np.full((n, self.n_ieq_constr), np.nan, dtype=float) - out["H_estm_error"] = np.full((n, self.n_eq_constr), np.nan, dtype=float) - - for target in self.targets: - label, k = target.label - out.get(label + "_estm_error")[:, k] = target.performance("mae") diff --git a/pysamoo/experimental/SACOBRA.py b/pysamoo/experimental/SACOBRA.py deleted file mode 100644 index 321479b..0000000 --- a/pysamoo/experimental/SACOBRA.py +++ /dev/null @@ -1,317 +0,0 @@ -import numpy as np -import scipy - -from pymoo.algorithms.soo.nonconvex.de import DE -from pymoo.algorithms.soo.nonconvex.ga import FitnessSurvival -from pymoo.constraints.tcv import TotalConstraintViolation -from pymoo.core.algorithm import Algorithm -from pymoo.core.population import Population -from pymoo.operators.sampling.lhs import LHS -from pymoo.operators.sampling.rnd import random -from pymoo.optimize import minimize -from pymoo.problems.meta import MetaProblem -from pymoo.util.display import SingleObjectiveDisplay -from pymoo.util.misc import cdist -from pymoo.util.normalization import NoNormalization -from pymoo.util.optimum import filter_optimum -from pysamoo.core.surrogate import Surrogate -from ezmodel.models.rbf import RBF -from ezmodel.util.transformation.plog import Plog -from ezmodel.util.transformation.zero_to_one import ZeroToOneNormalization - - -# ========================================================================================================= -# Display -# ========================================================================================================= - - -class SACOBRADisplay(SingleObjectiveDisplay): - - def _do(self, problem, evaluator, algorithm): - super()._do(problem, evaluator, algorithm) - self.output.append("eps", algorithm.eps) - self.output.append("rho", algorithm.rho if algorithm.rho is not None else "-") - self.output.append("C_feas", int(algorithm.C_feas)) - self.output.append("C_infeas", int(algorithm.C_infeas)) - - -class SACOBRA(Algorithm): - - def __init__(self, n_doe=None, display=SACOBRADisplay(), **kwargs): - super().__init__(display=display, **kwargs) - self.n_doe = n_doe - self.archive = Population() - - self.rho = None - - self.C_feas = 0 - self.C_infeas = 0 - - self.T_feas = None - self.T_infeas = None - - self.repair = None - - def _setup(self, problem, **kwargs): - - # directly rescale the whole problem - problem = Rescale(problem) - - if self.n_doe is None: - self.n_doe = 2 * problem.n_var + 1 - - xl, xu = problem.bounds() - l = (xu - xl).min() - self.eps = 0.005 * l - self.eps_max = 0.01 * l - - self.T_feas = np.floor(2 * np.sqrt(problem.n_var)) - self.T_infeas = self.T_feas - - # - models = {} - - for label, norm in [("default", NoNormalization()), ("plog", Plog())]: - model = RBF(kernel='cubic', tail='linear', normalized=False, norm_y=norm) - models[f"rbf-{label}"] = model - - targets = { - "F": ObjectivesAsTarget({k: v for (k, v) in models.items() if "plog" not in k}), - "G": ConstraintsAsTarget(models), - } - - self.surrogate = Surrogate(problem, targets) - - self.problem = problem - - def _initialize_infill(self): - return LHS().do(self.problem, self.n_doe) - - def _initialize_advance(self, infills=None, **kwargs): - self.archive = Population.merge(self.archive, infills) - - # analyze the initial population and find ranges - self._analyze_initial_pop(infills) - - # adjust the constraint values - self._adjust_constr(infills) - - # adjust the DRC parameter based on whether the obj. function is steep or not - self._adjust_drc() - - # initialize the surrogate with the doe points - self.surrogate.initialize(infills) - - def _infill(self): - - # update the current surrogate(s) based on the most recent update done after the last generation - self.surrogate.advance() - - # and now finally create the models - self.surrogate.fit(metric="spear", minimize=False) - - verbose = False - - if verbose: - - for target, entry in self.surrogate.targets["F"].predictor.predictors.items(): - print(target, entry["label"], entry["mae"]) - - for target, entry in self.surrogate.targets["G"].predictor.predictors.items(): - print(target, entry["label"], entry["mae"]) - - # get the current best solution and the infill - feas = self.archive.get("feas") - opt, sol = self.opt[0], self.archive[-1] - - # if the new solution has a better objective value than the current one, flag the next iteration for repair - if not np.any(feas): - - x_new = self._repair() - - elif not sol.feas and sol.f < opt.f: - - # attempt to repair the promising solution - x_new = self._repair(sol) - - # this is the regular iteration when obj. and constr. are used for optimization - else: - - # find the rho by cycling through the XI array - self.rho = self.XI[(self.n_gen - 1) % len(self.XI)] - - x_start = self._random_restart() - - # search on the surrogate (cobra iteration) - x_new = self._cobra(x_start, self.eps, self.rho) - - return Population.new(X=[x_new]) - - def _advance(self, infills=None, **kwargs): - - # adjust the constraint values - self._adjust_constr(infills) - - # adjust the eps value (margin for infeasibility) - self._adjust_margins(infills) - - # add them to the archive - self.archive = Population.merge(self.archive, infills) - - # add the new infill solutions to the surrogate to be used in the next iteration - self.surrogate.update(infills) - - def _analyze_initial_pop(self, pop): - f, G = pop.get("f", "G") - self.FR = f.max() - f.min() - self.GR = G.max(axis=0) - G.min(axis=0) - - def _adjust_constr(self, pop): - G = pop.get("G") - _G = G * (self.GR.mean() / self.GR) - pop.set("G", _G) - - def _adjust_margins(self, infills): - sol = infills[0] - - if sol.feas: - self.C_feas += 1 - self.C_infeas = 0 - else: - self.C_feas = 0 - self.C_infeas += 1 - - if self.C_feas >= self.T_feas: - self.eps = 0.5 * self.eps - self.C_feas = 0 - - if self.C_infeas >= self.T_infeas: - self.eps = min(2 * self.eps, self.eps_max) - self.C_infeas = 0 - - def _cobra(self, x_start, eps, rho): - - X = self.archive.get("X") - - tcv = TotalConstraintViolation(ieq_eps=-1 * eps) - - def func_obj(x): - return self.surrogate.evaluate(x, return_values_of=["F"])[0] - - def func_constr(x): - G = self.surrogate.evaluate(x, return_values_of=["G"]) - cv = tcv.calc(G) - return -1 * (cv[0]) - - def func_constr_trust(x): - closest = cdist(x[None, :], X).min() - trust = rho - closest - return -1 * trust - - constraints = [{"type": 'ineq', "fun": func_constr}, {"type": 'ineq', "fun": func_constr_trust}] - - xl, xu = self.problem.bounds() - bounds = np.column_stack([xl, xu]) - - options = {} - - res = scipy.optimize.minimize(func_obj, - x_start, - args=(), - bounds=bounds, - method='SLSQP', - constraints=constraints, - tol=None, - callback=None, - options=options) - - print(func_obj(res.x), func_constr(res.x), func_constr_trust(res.x)) - print(res.x) - return res.x - - def _random_restart(self): - p1 = 0.125 - p2 = 0.4 - - feas = self.archive.get("feas") - - if feas.sum() / len(feas) < 0.05: - p = p2 - else: - p = p1 - - if np.random.random() < p: - x = random(self.problem)[0] - else: - x = self.opt[0].X - - return x - - def _repair(self, sol=None): - - tcv = TotalConstraintViolation() - - class MyProblem(MetaProblem): - - def _evaluate(self, x, out, *args, **kwargs): - super()._evaluate(x, out, *args, **kwargs) - - if sol is not None: - out["F"] = cdist(x, sol.X[None, :]) - else: - out["F"] = tcv.calc(out["G"]) - out["G"][:] = 0.0 - - problem = MyProblem(self.surrogate) - - sampling = Population.new(X=self.archive.get("X")) - self.evaluator.eval(problem, sampling, count_evals=False) - - # let us start with a maximum of 100 solutions - if len(sampling) > 100: - sampling = FitnessSurvival().do(self.surrogate, sampling, n_survive=100) - - algorithm = DE(pop_size=len(sampling), sampling=sampling) - - res = minimize(problem, - algorithm, - return_least_infeasible=True) - - return res.X - - def _adjust_drc(self): - if self.FR > 1000: - self.XI = np.array([0.001, 1e-16]) - else: - self.XI = np.array([0.3, 0.05, 0.001, 0.0005, 1e-16]) - - def _set_optimum(self): - self.opt = filter_optimum(self.archive, least_infeasible=True) - - -class MinusOneToOneNormalization(ZeroToOneNormalization): - - def forward(self, X): - return super().forward(X) * 2 - 1 - - def backward(self, X): - return super().backward((X + 1) / 2) - - -class Rescale(MetaProblem): - - def __init__(self, problem): - super().__init__(problem) - assert self.xl is not None and self.xu is not None, "Both, xl and xu, must be set to redefine the problem!" - - self.norm = MinusOneToOneNormalization(problem.xl, problem.xu) - self.xl, self.xu = -np.ones(self.n_var), np.ones(self.n_var) - - def do(self, x, out, *args, **kwargs): - out["__X__"] = x - xp = self.norm.backward(x) - super().do(xp, out, *args, **kwargs) - - def _calc_pareto_set(self, *args, **kwargs): - ps = super()._calc_pareto_set(*args, **kwargs) - return self.norm.forward(ps) diff --git a/pysamoo/experimental/__init__.py b/pysamoo/experimental/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pysamoo/experimental/acquisition.py b/pysamoo/experimental/acquisition.py deleted file mode 100644 index f6d805a..0000000 --- a/pysamoo/experimental/acquisition.py +++ /dev/null @@ -1,102 +0,0 @@ -from pymoo.core.meta import Meta -from scipy.stats import norm - -# ========================================================================================================= -# Acquisition Functions -# ========================================================================================================= - - -class AcquisitionFunction: - - def calc(self, mu, sigma, **kwargs): - pass - - -class EI(AcquisitionFunction): - - def calc(self, mu, sigma, f_min=None, **kwargs): - if f_min is None: - raise Exception("Estimation of minimum function value needs to be provided!") - - f = - sigma - - # through precision error sigma can be negative - this should actually never be the case - pos_sigma = sigma > 0 - mu, sigma = mu[pos_sigma], sigma[pos_sigma] - - # minimization version of EI - impr = f_min - mu - - # calculate expected improvement - z = impr / sigma - - ei = impr * norm.cdf(z) + sigma * norm.pdf(z) - - # because we are minimizing take the negative expected improvement - f[pos_sigma] = - ei - - return f - - -class POI(AcquisitionFunction): - - def calc(self, mu, sigma, f_min=None, **kwargs): - if f_min is None: - raise Exception("Estimation of minimum function value needs to be provided!") - - pos_sigma = sigma > 0 - mu, sigma = mu[pos_sigma], sigma[pos_sigma] - f = sigma - - # minimization version of PI - impr = f_min - mu - - # calculate pi - z = impr / sigma - pi = norm.pdf(z) - - f[pos_sigma] = - pi - - return f - - -class UCB(AcquisitionFunction): - - def __init__(self, beta=3.0) -> None: - super().__init__() - self.beta = beta - - def calc(self, mu, sigma, **kwargs): - ucb = mu - self.beta * sigma - return ucb - - -# ========================================================================================================= -# Acquisition Problem -# ========================================================================================================= - - -class AcquisitionProblem(Meta): - - def __init__(self, - problem, - model, - acquisition_func, - **kwargs): - super().__init__(problem) - self.model = model - self.acquisition_func = acquisition_func - self.kwargs = kwargs - - def _evaluate(self, x, out, *args, **kwargs): - if self.model is None: - raise Exception("Please set the model for the problem to be defined.") - - # calculate the metric using the implementation - mu, sigma = self.model.predict(x, return_values_of=["y", "sigma"]) - mu, sigma = mu[:, 0], sigma[:, 0] - - # calculate the value of the acquisition function - acq = self.acquisition_func.calc(mu, sigma, x=x, **self.kwargs) - - out["F"], out["acq"] = acq, acq diff --git a/pysamoo/experimental/bo.py b/pysamoo/experimental/bo.py deleted file mode 100644 index 5f026e7..0000000 --- a/pysamoo/experimental/bo.py +++ /dev/null @@ -1,206 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np -from ezmodel.core.factory import models_from_clazzes -from ezmodel.core.selection import ModelSelection -from ezmodel.models.kriging import Kriging -from ezmodel.util.partitioning.crossvalidation import CrossvalidationPartitioning -from pymoo.algorithms.soo.nonconvex.ga import FitnessSurvival -from pymoo.algorithms.soo.nonconvex.ga_niching import NicheGA -from pymoo.core.callback import Callback -from pymoo.core.population import Population -from pymoo.operators.sampling.lhs import LHS -from pymoo.optimize import minimize -from pymoo.termination.default import DefaultSingleObjectiveTermination -from pymoo.util.display.column import Column - -from pymoo.util.display.output import Output -from pymoo.util.display.single import SingleObjectiveOutput -from pymoo.util.normalization import ZeroToOneNormalization - -from pysamoo.core.algorithm import SurrogateAssistedAlgorithm -from pysamoo.experimental.acquisition import EI, AcquisitionProblem - - -# --------------------------------------------------------------------------------------------------------- -# Display -# --------------------------------------------------------------------------------------------------------- - - -class EGOOutput(SingleObjectiveOutput): - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.output = SingleObjectiveOutput() - - self.f_new = Column(name="f_new") - self.acq = Column(name="acq") - - self.model = [Column(name="acq", func=lambda a: a.surrogate.regr), - Column(name="corr", func=lambda a: a.surrogate.corr), - Column(name="ARD", func=lambda a: a.surrogate.ARD) - ] - - def initialize(self, algorithm): - self.output.initialize(algorithm) - self.columns = self.output.columns + [self.f_new, self.acq] - if algorithm.model_selection: - self.columns += self.model - - def update(self, algorithm): - bo = algorithm - self.output.update(bo) - - if algorithm.acq is not None: - - self.f_new.set(bo.infills.get("F").min()) - self.acq.set(bo.infills.get("acq").min()) - - if bo.model_selection: - for col in self.model: - col.update(bo) - - -# --------------------------------------------------------------------------------------------------------- -# Implementation -# --------------------------------------------------------------------------------------------------------- - -class BayesianOptimization(SurrogateAssistedAlgorithm): - - def __init__(self, - acq_func=EI(), - model_selection=False, - adaptive_fmin=True, - output=EGOOutput(), - **kwargs): - - super().__init__(output=output, **kwargs) - self.default_termination = DefaultSingleObjectiveTermination() - - self.model_selection = model_selection - self.acq_func = acq_func - self.adaptive_fmin = adaptive_fmin - self.acq = None - - def _infill(self): - - # get all the points that have been evaluated yet - X, F = self._archive.get("X", "F") - - # get the problem and the boundaries - problem = self.problem - xl, xu = problem.bounds() - - # the defaults for surrogate modeling - normalize the values to be between zero and one - defaults = dict(norm_X=ZeroToOneNormalization(xl, xu)) - - # rather the best model should be selected or simply the default kriging implementation taken - if self.model_selection: - models = models_from_clazzes(Kriging, **defaults) - partitions = CrossvalidationPartitioning(k_folds=5, seed=1).do(X) - model = ModelSelection(models).do(X, F[:, 0], partitions) - else: - model = Kriging(regr="linear", corr="gauss", ARD=True, **defaults) - model.fit(X, F[:, 0]) - - if self.adaptive_fmin: - - # get the acquisition problem to be optimized - acq = robust_fmin_acquisition(problem, model, self.acq_func, self._archive) - - else: - # just use the minimum (even though this can lead to precision issues) - _min = F[:, 0].argmin() - f_min = model.predict(X[_min])[0, 0] - acq = AcquisitionProblem(problem, model, self.acq_func, f_min=f_min) - - # use a bigger latin hypercube in the beginning because the problem might be highly multi-modal - sampling = LHS().do(problem, 500) - - algorithm = NicheGA(pop_size=50, sampling=sampling) - - termination = DefaultSingleObjectiveTermination(period=1) - - res = minimize(acq, - algorithm, - termination, - verbose=False - ) - - X, F = res.opt.get("X", "F") - - self.acq = acq - self.surrogate = model - return Population.new(X=X, acq=F)[[0]] - - def _set_optimum(self): - self.opt = FitnessSurvival().do(self.problem, self._archive, n_survive=1) - - -def robust_fmin_acquisition(problem, model, acq_func, points): - X, F = points.get("X", "F") - - sorted_by_pred = np.sort(model.predict(X)[:, 0]) - - n_intervals = min(20, len(X)) - n_points = 500 - - interval = int(len(sorted_by_pred) / n_intervals) - - for cnt in range(n_intervals): - - # increase the index for f_min in each iteration - f_min = sorted_by_pred[cnt * interval] - - # create the acquisition problem and radomly sample - acq = AcquisitionProblem(problem, model, acq_func, f_min=f_min) - sampling = LHS().do(problem, n_points) - - # the maximum value found of during random sampling - max_prob_imprv = (- acq.evaluate(sampling.get("X"))).max() - - # print(cnt, sorted_by_pred[0], f_min, max_prob_imprv) - - # if the value is not very small then the fmin is okay to be used - if max_prob_imprv > 1e-2: - break - - return acq - - -class EGOVisualization(Callback): - - def notify(self, algorithm): - problem = algorithm.problem - if problem.n_var > 1 or problem.n_obj > 1 or algorithm.surrogate is None: - return - - fig = plt.figure() - - gs = fig.add_gridspec(4, 1) - plt_func = fig.add_subplot(gs[:3]) - plt_acq = fig.add_subplot(gs[3]) - - X = algorithm.pop.get("X") - F = problem.evaluate(X) - infill = algorithm.infills[0] - plt_func.scatter(X, F, color="red") - acq = algorithm.acq - - mesh = np.linspace(problem.xl[0], problem.xu[0], 1000)[:, None] - - gp = algorithm.surrogate - mu, sigma = gp.predict(mesh, return_values_of=["y", "sigma"]) - - plt_func.fill_between(mesh[:, 0], (mu - 2 * sigma)[:, 0], (mu + 2 * sigma)[:, 0], alpha=0.2, color='k') - - plt_func.scatter(infill.X, infill.F, color="red", s=100, marker="x") - plt_func.plot(mesh, gp.predict(mesh), color="red") - - plt_func.axvline(x=algorithm.infills[0].X, color="black", linestyle='dashed') - - plt_func.plot(mesh, problem.evaluate(mesh), color="black") - - plt_acq.plot(mesh, acq.evaluate(mesh), color="blue") - plt_acq.scatter(infill.X, acq.evaluate(infill.X), color="red", s=100, marker="x") - - plt.show() diff --git a/pysamoo/sampling/__init__.py b/pysamoo/sampling/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pysamoo/sampling/energy.py b/pysamoo/sampling/energy.py deleted file mode 100644 index 6287f30..0000000 --- a/pysamoo/sampling/energy.py +++ /dev/null @@ -1,76 +0,0 @@ -import numpy as np - -from pymoo.core.sampling import Sampling -from pymoo.util.normalization import normalize, denormalize -from pymoo.util.ref_dirs.energy import squared_dist, calc_potential_energy_with_grad -from pymoo.util.ref_dirs.optimizer import Adam -from pysamoo.sampling.niching import NichingConstrainedSampling -from pysamoo.sampling.rejection import RejectionConstrainedSampling - - -def calc_potential_energy(A, d): - i, j = np.triu_indices(len(A), 1) - D = np.sqrt(squared_dist(A, A)[i, j]) - energy = np.log((1 / D ** d).mean()) - return energy - - -class EnergyConstrainedSampling(Sampling): - - def __init__(self, - func_eval_constr, - n_max_iter=10000): - super().__init__() - self.func_eval_constr = func_eval_constr - self.n_max_iter = n_max_iter - - def _do(self, problem, n_samples, **kwargs): - xl, xu = problem.bounds() - constr = self.func_eval_constr - d = problem.n_var ** 2 - - X = RejectionConstrainedSampling(constr).do(problem, n_samples).get("X") - if len(X) < n_samples: - X = NichingConstrainedSampling(constr).do(problem, n_samples).get("X") - - if len(X) == 0: - raise Exception("No feasible solution could be found!") - elif len(X) < n_samples: - print("WARNING: Less feasible solutions than requested could be found.") - - X = normalize(X, xl, xu) - - optimizer = Adam(alpha=0.005) - - obj, grad = calc_potential_energy_with_grad(X, d) - hist = [obj] - - done = False - - for i in range(self.n_max_iter): - - if done: - break - - _X = optimizer.next(X, grad) - _CV = constr(denormalize(_X, xl, xu)) - feasible = np.logical_and(_CV <= 0, np.all(np.logical_and(_X >= 0, _X <= 1), axis=1)) - - X[feasible] = _X[feasible] - - obj, grad = calc_potential_energy_with_grad(X, d) - hist.append(obj) - - hist = hist[-100:] - - avg_impr = (- np.diff(hist[-100:])).mean() - - if len(hist) > 100: - if avg_impr < 1e-3: - optimizer = Adam(alpha=optimizer.alpha / 2) - elif avg_impr < 1e-6: - done = True - - X = denormalize(X, xl, xu) - - return X diff --git a/pysamoo/usage/usage_bayesian_optimization.py b/pysamoo/usage/usage_bayesian_optimization.py deleted file mode 100644 index b1079f7..0000000 --- a/pysamoo/usage/usage_bayesian_optimization.py +++ /dev/null @@ -1,18 +0,0 @@ -from pymoo.optimize import minimize -from pymoo.problems.single import Sphere -from pysamoo.experimental.bo import BayesianOptimization - -if __name__ == "__main__": - problem = Sphere(n_var=10) - - algorithm = BayesianOptimization(model_selection=True, adaptive_fmin=True) - - res = minimize(problem, - algorithm, - ("n_gen", 150), - seed=1, - verbose=True) - - print("Best solution found: \nX = %s\nF = %s\nCV=%s" % (res.X, res.F, res.CV)) - - diff --git a/pysamoo/usage/usage_smac.py b/pysamoo/usage/usage_smac.py deleted file mode 100644 index f2258c7..0000000 --- a/pysamoo/usage/usage_smac.py +++ /dev/null @@ -1,20 +0,0 @@ -from pymoo.optimize import minimize -from pymoo.problems.single import Sphere -from pysamoo.vendor.smac import SMAC - -if __name__ == "__main__": - - problem = Sphere(n_var=10) - - algorithm = SMAC() - - res = minimize( - problem, - algorithm, - ('n_evals', 60), - seed=2, - verbose=True) - - print("Best solution found: \nX = %s\nF = %s\nCV=%s" % (res.X, res.F, res.CV)) - - diff --git a/pysamoo/vendor/__init__.py b/pysamoo/vendor/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pysamoo/vendor/ego.py b/pysamoo/vendor/ego.py deleted file mode 100644 index 5ca5864..0000000 --- a/pysamoo/vendor/ego.py +++ /dev/null @@ -1,52 +0,0 @@ -import numpy as np -from pymoo.util.display.single import SingleObjectiveOutput - -try: - from GPyOpt.methods import BayesianOptimization -except: - raise Exception("gpyopt not found. Please execute: 'pip install gpyopt'") - -from pymoo.core.algorithm import Algorithm -from pymoo.core.individual import Individual -from pymoo.core.population import Population - - -class EGO(Algorithm): - - def __init__(self, - output=SingleObjectiveOutput(), - **kwargs): - super().__init__(output=output, **kwargs) - self.domain, self.func = None, None - - def _setup(self, problem, **kwargs): - - domain = [] - for k in range(problem.n_var): - e = {'name': f"var_{k}", 'type': 'continuous', 'domain': (problem.xl[k], problem.xu[k])} - domain.append(e) - - def func(x): - ind = Individual(X=x[0]) - self.evaluator.eval(self.problem, ind) - self.pop = Population.merge(self.pop, Population.create(ind)) - return ind.F[0] - - self.obj = BayesianOptimization(f=func, domain=domain) - - def _initialize_advance(self, **kwargs): - self._advance() - - def _advance(self, **kwargs): - obj = self.obj - - try: - obj._update_model(obj.normalization_type) - except np.linalg.linalg.LinAlgError: - pass - - obj.num_acquisitions += 1 - obj.suggested_sample = obj._compute_next_evaluations() - obj.X = np.vstack((obj.X, obj.suggested_sample)) - obj.evaluate_objective() - diff --git a/pysamoo/vendor/smac.py b/pysamoo/vendor/smac.py deleted file mode 100644 index 44bba15..0000000 --- a/pysamoo/vendor/smac.py +++ /dev/null @@ -1,86 +0,0 @@ -import numpy as np -from pymoo.util.display.single import SingleObjectiveOutput - -try: - from smac.facade.func_facade import fmin_smac -except: - raise Exception("smac not found. Please execute: 'pip install smac'") - - - -from pymoo.algorithms.base.local import LocalSearch -from pymoo.core.individual import Individual -from pymoo.core.population import Population -from pymoo.operators.sampling.lhs import LatinHypercubeSampling -from pymoo.util.optimum import filter_optimum - - -class FunctionCall: - - def __init__(self, problem) -> None: - super().__init__() - self.problem = problem - - def __call__(self, x): - return self.problem.evaluate(x[None, :])[0, 0] - - -class SMAC(LocalSearch): - - def __init__(self, x0=None, - sampling=LatinHypercubeSampling(), - output=SingleObjectiveOutput(), - n_sample_points="auto", - n_max_sample_points=50, - **kwargs): - - super().__init__(x0, sampling, n_sample_points, n_max_sample_points, output=output, **kwargs) - - self.cnt = 0 - self.history = None - - def _local_advance(self, **kwargs): - - if self.history is None: - self.history = self._optimize() - - self.evaluator.n_eval += 1 - self.pop = self.history.pop(0) - self.cnt += 1 - - if len(self.history) == 0: - self.termination.force_termination = True - - def _optimize(self): - - problem = self.problem - - func = FunctionCall(problem) - - seed = np.random.randint(0, 2 ** 32 - 1) - - eval_remaininig = self.termination.n_max_evals - self.evaluator.n_eval - - x, cost, obj = fmin_smac(func=func, - x0=self.x0.X, - bounds=np.column_stack(problem.bounds()), - maxfun=eval_remaininig, - rng=seed, - scenario_args=dict(output_dir=None)) - - pop = [] - - for k, v in obj.runhistory.data.items(): - config_id = k.config_id - f = v.cost - x = np.array(list(obj.runhistory.ids_config[config_id]._values.values())) - ind = Individual(X=x, F=np.array([f]), CV=np.array([0.0]), feasible=[True]) - pop.append(Population.create(ind)) - - return pop - - def _set_optimum(self): - pop = self.pop if self.opt is None else Population.merge(self.opt, self.pop) - self.opt = filter_optimum(pop, least_infeasible=True) - - diff --git a/pysamoo/version.py b/pysamoo/version.py deleted file mode 100644 index b3f4756..0000000 --- a/pysamoo/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.1.2" diff --git a/setup.py b/setup.py index 08d2961..fb6a74c 100644 --- a/setup.py +++ b/setup.py @@ -1,38 +1,45 @@ +import os + import setuptools -from pysamoo.version import __version__ +# Read the version without importing the package (src/ layout: package not on +# sys.path at build time). +__version__ = {} +with open(os.path.join("src", "pysamoo", "version.py")) as f: + exec(f.read(), __version__) +__version__ = __version__["__version__"] # --------------------------------------------------------------------------------------------------------- # GENERAL # --------------------------------------------------------------------------------------------------------- -__name__ = "pysamoo" -__author__ = "Julian Blank" -__url__ = "https://anyoptimization.com/projects/pysamoo/" +name = "pysamoo" +author = "Julian Blank" +url = "https://anyoptimization.com/projects/pysamoo/" data = dict( - name=__name__, + name=name, version=__version__, - author=__author__, - url=__url__, - python_requires='>=3.7', + author=author, + url=url, + python_requires='>=3.10', author_email="blankjul@msu.edu", description="Surrogate-Assisted Multi-objective Optimization", - license='GNU AFFERO GENERAL PUBLIC LICENSE (AGPL)', + license='PolyForm Noncommercial License 1.0.0', keywords="surrogate, metamodel, bayesian optimization", - install_requires=["pymoo==0.6.1.1", "ezmodel"], + install_requires=["pymoo>=0.6.1.5,<0.6.2", "ezmodel"], + extras_require={ + "dev": ["ruff", "mypy", "pytest", "pytest-xdist", "pytest-cov"], + }, platforms='any', classifiers=[ 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', 'Operating System :: OS Independent', - 'License :: OSI Approved :: Apache Software License', + 'License :: Other/Proprietary License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Topic :: Scientific/Engineering', @@ -53,11 +60,8 @@ def readme(): return f.read() -def packages(): - return ["pysamoo"] + ["pysamoo." + e for e in setuptools.find_packages(where='pysamoo')] - - data['long_description'] = readme() -data['packages'] = packages() +data['package_dir'] = {'': 'src'} +data['packages'] = setuptools.find_packages(where='src') setuptools.setup(**data) diff --git a/src/pysamoo/__init__.py b/src/pysamoo/__init__.py new file mode 100644 index 0000000..5c29f14 --- /dev/null +++ b/src/pysamoo/__init__.py @@ -0,0 +1 @@ +"""pysamoo — surrogate-assisted multi-objective optimization built on pymoo.""" diff --git a/src/pysamoo/algorithms/__init__.py b/src/pysamoo/algorithms/__init__.py new file mode 100644 index 0000000..f86cbc5 --- /dev/null +++ b/src/pysamoo/algorithms/__init__.py @@ -0,0 +1 @@ +"""Surrogate-assisted optimization algorithms (GPSAF, PSAF, SSANSGA2, and the EGO-style family).""" diff --git a/src/pysamoo/algorithms/_ego.py b/src/pysamoo/algorithms/_ego.py new file mode 100644 index 0000000..c65966e --- /dev/null +++ b/src/pysamoo/algorithms/_ego.py @@ -0,0 +1,125 @@ +"""Shared machinery for the EGO-style surrogate-assisted algorithms (fit, predict, candidate pools).""" + +from copy import deepcopy + +import numpy as np +from pymoo.algorithms.soo.nonconvex.ga import FitnessSurvival +from pymoo.indicators.hv import HV +from pymoo.operators.sampling.lhs import LHS +from pymoo.util.nds.non_dominated_sorting import NonDominatedSorting +from pysurrogate.dace import Exponential +from pysurrogate.models import Kriging + + +def default_kriging(): + """The default per-objective surrogate prototype (Kriging with an exponential/Matern-1/2 kernel). + + Returns: + A fresh, unfitted ``Kriging`` model to be deep-copied per objective. + """ + return Kriging(corr=Exponential()) + + +def fit_per_objective(proto, X, F): + """Deep-copy the prototype once per objective and fit each copy on that objective's column. + + Args: + proto: The surrogate prototype to clone. + X: Decision matrix, shape ``(n, n_var)``. + F: Objective matrix, shape ``(n, n_obj)``. + + Returns: + A list of fitted models, one per objective column of ``F``. + """ + models = [deepcopy(proto) for _ in range(F.shape[1])] + for m, model in enumerate(models): + model.fit(X, F[:, m]) + return models + + +def predict_mu_sigma(models, X): + """Predict the mean and standard deviation of each model at ``X`` in a single pass per model. + + ``predict(X, var=True)`` populates both the mean (``.y``) and the standard deviation (``.sigma``), + so one call per model suffices (the earlier code predicted each candidate twice). + + Args: + models: The fitted per-objective models. + X: The points to predict at, shape ``(n, n_var)``. + + Returns: + A tuple ``(mu, sigma)``, each of shape ``(n, len(models))``. + """ + preds = [model.predict(X, var=True) for model in models] + mu = np.column_stack([p.y[:, 0] for p in preds]) + sigma = np.column_stack([p.sigma[:, 0] for p in preds]) + return mu, sigma + + +def lhs_local_pool(problem, elite_X, n_pool, rng, scale=0.05): + """A candidate pool: a space-filling LHS plus Gaussian perturbations of the elite designs. + + A pure LHS pool is too sparse in higher dimensions to locate the acquisition optimum, so half the + pool is drawn near the current best (elite) designs. The RNG is drawn strictly in the order + LHS -> integers -> standard_normal so results are reproducible for a fixed seed. + + Args: + problem: The problem (for its box bounds and dimensionality). + elite_X: The elite designs to perturb around, shape ``(k, n_var)``. + n_pool: Size of each half of the pool (LHS and local); the result has ``2 * n_pool`` rows. + rng: The run's numpy ``Generator``. + scale: Perturbation width as a fraction of the box width. + + Returns: + The candidate decision matrix, shape ``(2 * n_pool, n_var)``. + """ + xl, xu = problem.xl, problem.xu + cand = LHS().do(problem, n_pool, random_state=rng).get("X") + idx = rng.integers(len(elite_X), size=n_pool) + local = np.clip(elite_X[idx] + scale * (xu - xl) * rng.standard_normal((n_pool, problem.n_var)), xl, xu) + return np.vstack([cand, local]) + + +def front_and_hv(F, margin=0.1, eps=1e-9): + """The current non-dominated front and a hypervolume indicator referenced past its nadir. + + Args: + F: Objective matrix, shape ``(n, n_obj)``. + margin: Fraction of the objective range added past the nadir for the reference point. + eps: Floor on the per-objective range (guards against a degenerate zero-width front). + + Returns: + A tuple ``(nds, front, hv)``: the non-dominated indices, the front ``F[nds]``, and a + ``pymoo`` ``HV`` indicator with the reference point already set. + """ + nds = NonDominatedSorting().do(F, only_non_dominated_front=True) + front = F[nds] + z_min, z_max = F.min(axis=0), F.max(axis=0) + ref = z_max + margin * np.maximum(z_max - z_min, eps) + return nds, front, HV(ref_point=ref) + + +def pareto_optimum(archive): + """The non-dominated subset of an archive (the reported optimum for multi-objective methods). + + Args: + archive: The evaluated population. + + Returns: + The non-dominated members of ``archive``. + """ + nds = NonDominatedSorting().do(archive.get("F"), only_non_dominated_front=True) + return archive[nds] + + +def best_optimum(problem, archive): + """The single best (fitness-survival) member of an archive (the optimum for single-objective methods). + + Args: + problem: The problem (needed by pymoo's survival). + archive: The evaluated population. + + Returns: + A population of one -- the best solution. + """ + return FitnessSurvival().do(problem, archive, n_survive=1) diff --git a/src/pysamoo/algorithms/csea.py b/src/pysamoo/algorithms/csea.py new file mode 100644 index 0000000..5b853a6 --- /dev/null +++ b/src/pysamoo/algorithms/csea.py @@ -0,0 +1,96 @@ +"""CSEA -- Classification-based Surrogate-assisted Evolutionary Algorithm for expensive many-objective problems.""" + +import numpy as np +from pymoo.core.population import Population +from pymoo.util.display.multi import MultiObjectiveOutput +from pymoo.util.nds.non_dominated_sorting import NonDominatedSorting +from sklearn.neighbors import KNeighborsClassifier + +from pysamoo.algorithms._ego import pareto_optimum +from pysamoo.core.algorithm import SurrogateAssistedAlgorithm + + +class CSEA(SurrogateAssistedAlgorithm): + """CSEA (Pan et al., 2019): a *classification* surrogate that screens offspring for evaluation. + + Instead of regressing each objective, CSEA trains a single classifier to answer a cheaper + question -- "is this design one of the good ones?" -- which sidesteps the accuracy demands of + regression and scales naturally to many objectives. Each iteration labels the archive (good = + better-than-median non-dominated rank), fits a K-nearest-neighbour classifier on the decision + vectors, generates a large pool of genetic offspring (recombination + mutation of the archive), + and evaluates the ``n_infills`` offspring the classifier judges most likely to be good. Reuses + scikit-learn's classifier and pymoo's non-dominated sorting. + + Args: + n_infills: True-function evaluations selected per iteration. + n_offspring: Genetic offspring generated and classified per iteration. + n_neighbors: Neighbours for the K-nearest-neighbour classifier. + p_mut: Per-coordinate mutation probability (defaults to ``1/n_var``). + """ + + # uses a KNN classification surrogate, not the base default model pool. + build_default_surrogate = False + + def __init__(self, n_infills=5, n_offspring=200, n_neighbors=5, p_mut=None, output=None, **kwargs): + super().__init__(output=output if output is not None else MultiObjectiveOutput(), **kwargs) + self.n_infills = n_infills + self.n_offspring = n_offspring + self.n_neighbors = n_neighbors + self.p_mut = p_mut + + def _offspring(self, X, xl, xu, rng): + # genetic offspring: blend recombination of two random parents + Gaussian mutation + n, d = self.n_offspring, X.shape[1] + p1, p2 = X[rng.integers(len(X), size=n)], X[rng.integers(len(X), size=n)] + beta = rng.random((n, d)) + child = beta * p1 + (1.0 - beta) * p2 + p_mut = self.p_mut if self.p_mut is not None else 1.0 / d + mut = rng.random((n, d)) < p_mut + child = child + mut * (0.1 * (xu - xl) * rng.standard_normal((n, d))) + return np.clip(child, xl, xu) + + def _infill(self): + X, F = self._archive.get("X", "F") + problem = self.problem + xl, xu = problem.xl, problem.xu + span = np.maximum(xu - xl, 1e-12) + rng = self.random_state + + cand = self._offspring(X, xl, xu, rng) + + # label the archive: "good" = non-dominated rank at or below the median rank + good = self.label_good(F) + + # if the labels are degenerate (all one class), no classifier is possible -> pick at random + if good.all() or (~good).all(): + sel = rng.permutation(len(cand))[: self.n_infills] + return Population.new(X=cand[sel]) + + # KNN classifier on normalized decision vectors; score offspring by P(good) + clf = KNeighborsClassifier(n_neighbors=min(self.n_neighbors, int(good.sum()), int((~good).sum())) or 1) + clf.fit((X - xl) / span, good.astype(int)) + prob = clf.predict_proba((cand - xl) / span) + good_col = list(clf.classes_).index(1) if 1 in clf.classes_ else 0 + score = prob[:, good_col] + + sel = np.argsort(-score)[: self.n_infills] + return Population.new(X=cand[sel]) + + @staticmethod + def label_good(F): + """Binary "good" label per objective vector: non-dominated rank at or below the median rank. + + Exposed as a static method so the classification target can be checked directly against + non-dominated ranks in the fidelity tests. + + Args: + F: Objective matrix, shape ``(n, n_obj)``. + + Returns: + A boolean array, ``True`` for the "good" (better-than-median-rank) rows. + """ + ranks = NonDominatedSorting().do(F, return_rank=True)[1] + return ranks <= np.median(ranks) + + def _set_optimum(self): + self.opt = pareto_optimum(self._archive) diff --git a/src/pysamoo/algorithms/ehvi.py b/src/pysamoo/algorithms/ehvi.py new file mode 100644 index 0000000..979a814 --- /dev/null +++ b/src/pysamoo/algorithms/ehvi.py @@ -0,0 +1,106 @@ +"""EHVI -- Expected Hypervolume Improvement Bayesian optimization for multi-objective problems.""" + +import numpy as np +from pymoo.core.population import Population +from pymoo.util.display.multi import MultiObjectiveOutput + +from pysamoo.algorithms._ego import ( + default_kriging, + fit_per_objective, + front_and_hv, + lhs_local_pool, + pareto_optimum, + predict_mu_sigma, +) +from pysamoo.core.algorithm import SurrogateAssistedAlgorithm + + +class EHVI(SurrogateAssistedAlgorithm): + """Expected Hypervolume Improvement BO: pick the point that most improves the Pareto hypervolume. + + Fits one Kriging model per objective on the archive, then chooses the next point by maximizing + the *Expected Hypervolume Improvement* -- the expected growth of the current non-dominated front's + hypervolume if the point were evaluated. EHVI is estimated by Monte-Carlo (sample the independent + per-objective Gaussian posteriors, average the hypervolume improvement), which needs no exact-EHVI + cell decomposition and works for any number of objectives. + + For speed the candidate pool is first *screened* by an optimistic ``mu - sigma`` point's + hypervolume improvement (cheap, one HV call each), and the Monte-Carlo estimate is computed only + on the most promising ``n_screen`` candidates. Reuses pymoo's HV indicator and non-dominated + sorting and the pysurrogate Kriging surrogate (Kriging supplies the predictive ``sigma``). + + Args: + pool: LHS candidate-pool size drawn per infill. + n_screen: How many pool candidates (best optimistic HVI) get the Monte-Carlo EHVI estimate. + n_samples: Monte-Carlo posterior samples per screened candidate. + surrogate: pysurrogate Kriging prototype per objective (default ``Kriging(Exponential())``). + """ + + # manages its own per-objective Kriging models -> skip the base default-surrogate build. + build_default_surrogate = False + + def __init__(self, pool=200, n_screen=20, n_samples=32, surrogate=None, output=None, **kwargs): + super().__init__(output=output if output is not None else MultiObjectiveOutput(), **kwargs) + self.pool = pool + self.n_screen = n_screen + self.n_samples = n_samples + self.surrogate_proto = surrogate if surrogate is not None else default_kriging() + + def _infill(self): + X, F = self._archive.get("X", "F") + problem = self.problem + + # one Kriging per objective + models = fit_per_objective(self.surrogate_proto, X, F) + + # current non-dominated front and a reference point (nadir + 10% margin) for the hypervolume + nds, front, hv = front_and_hv(F) + hv0 = float(hv(front)) + + def hvi(point): + return max(0.0, float(hv(np.vstack([front, point]))) - hv0) + + # candidate pool: a space-filling LHS *plus* local perturbations of the current + # non-dominated designs. In higher dimensions a pure LHS pool is too sparse to locate the + # acquisition optimum, so seeding the neighbourhood of the front is what makes EHVI competitive. + cand = lhs_local_pool(problem, X[nds], self.pool, self.random_state) + mu, sigma = predict_mu_sigma(models, cand) + + # cheap screen: the optimistic point mu - sigma (minimization) rewards both a good mean and + # high uncertainty, so its HVI is a fast proxy for where EHVI is worth estimating. + opt_hvi = np.array([hvi(mu[i] - sigma[i]) for i in range(len(cand))]) + top = np.argsort(-opt_hvi)[: self.n_screen] + + # Monte-Carlo EHVI on the screened candidates + ehvi = np.array( + [self.expected_hvi(mu[i], sigma[i], front, hv, hv0, self.n_samples, self.random_state) for i in top] + ) + + x_best = cand[top[int(ehvi.argmax())]] + return Population.new(X=x_best[None, :]) + + @staticmethod + def expected_hvi(mu, sigma, front, hv, hv0, n_samples, random_state): + """Monte-Carlo Expected Hypervolume Improvement of one Gaussian-predicted candidate. + + Averages ``max(0, HV(front + sample) - hv0)`` over ``n_samples`` draws from the independent + per-objective Gaussian posterior ``N(mu, diag(sigma^2))``. Exposed as a static method so the + acquisition can be checked against exact 2-objective EHVI / grid quadrature (fidelity tests). + + Args: + mu: Predicted objective means of the candidate, shape ``(n_obj,)``. + sigma: Predictive standard deviations of the candidate, shape ``(n_obj,)``. + front: The current non-dominated objective vectors, shape ``(k, n_obj)``. + hv: A ``pymoo`` ``HV`` indicator with the reference point already set. + hv0: The hypervolume of ``front`` (so it is not recomputed per sample). + n_samples: Number of Monte-Carlo posterior samples. + random_state: A numpy ``Generator`` for the samples. + + Returns: + The Monte-Carlo EHVI estimate (a float). + """ + samples = mu + sigma * random_state.standard_normal((n_samples, len(mu))) + return float(np.mean([max(0.0, float(hv(np.vstack([front, s]))) - hv0) for s in samples])) + + def _set_optimum(self): + self.opt = pareto_optimum(self._archive) diff --git a/pysamoo/algorithms/gpsaf.py b/src/pysamoo/algorithms/gpsaf.py similarity index 58% rename from pysamoo/algorithms/gpsaf.py rename to src/pysamoo/algorithms/gpsaf.py index 9b168c0..d7a1c51 100644 --- a/pysamoo/algorithms/gpsaf.py +++ b/src/pysamoo/algorithms/gpsaf.py @@ -1,7 +1,7 @@ -import random +"""GPSAF — the generalized probabilistic surrogate-assisted framework algorithm.""" + from copy import deepcopy -import matplotlib.pyplot as plt import numpy as np from pymoo.algorithms.moo.nsga2 import RankAndCrowdingSurvival from pymoo.algorithms.soo.nonconvex.ga import FitnessSurvival @@ -15,23 +15,18 @@ from pymoo.util.display.column import Column from pymoo.util.display.output import Output from pymoo.util.dominator import get_relation -from pymoo.util.misc import norm_eucl_dist, cdist +from pymoo.util.misc import cdist, norm_eucl_dist from pymoo.util.optimum import filter_optimum -from pymoo.visualization.fitness_landscape import FitnessLandscape -from pymoo.visualization.video.callback_video import AnimationCallback from pysamoo.core.algorithm import SurrogateAssistedAlgorithm from pysamoo.core.knockout import noisy - # ========================================================================================================= # Display # ========================================================================================================= - class GPSAFOutput(Output): - def __init__(self, output, **kwargs): super().__init__(**kwargs) self.output = output @@ -47,7 +42,7 @@ def __init__(self, output, **kwargs): def initialize(self, algorithm): self.output.initialize(algorithm) - self.columns = [e for e in self.output.columns if e.name != 'f_avg'] + [self.n_influenced] + self.columns = [e for e in self.output.columns if e.name != "f_avg"] + [self.n_influenced] problem = algorithm.problem if problem.n_obj == 1 and problem.n_constr == 0: @@ -66,11 +61,11 @@ def update(self, algorithm): n_influenced = sum(surr_infills.get("type") == "trace") self.n_influenced.set(f"{n_influenced}/{len(surr_infills)}") - if self.mode == 'soo': + if self.mode == "soo": target = gpasf.surrogate.targets[0] self.mae.set(target.performance("mae")) self.model.set(target.best) - elif self.mode == 'moo': + elif self.mode == "moo": perf = gpasf.surrogate.performance("mae") if ("F", 0) in perf: self.mae_f1.set(perf[("F", 0)]) @@ -78,76 +73,13 @@ def update(self, algorithm): self.mae_f2.set(perf[("F", 1)]) -# ========================================================================================================= -# Animation -# ========================================================================================================= - -class GPSAFAnimation(AnimationCallback): - - def __init__(self, - nth_gen=1, - n_samples_for_surface=200, - dpi=200, - **kwargs): - - super().__init__(nth_gen=nth_gen, dpi=dpi, **kwargs) - self.n_samples_for_surface = n_samples_for_surface - self.last_pop = None - - def do(self, problem, algorithm): - - if problem.n_var != 2 or problem.n_obj != 1: - raise Exception( - "This visualization can only be used for problems with two variables and one objective!") - - # draw the problem surface - doe = algorithm.surrogate.targets["F"].doe - if doe is not None: - problem = algorithm.surrogate - - plot = FitnessLandscape(problem, _type="contour", kwargs_contour=dict(alpha=0.5)) - plot.do() - - if doe is not None: - plt.scatter(doe.get("X")[:, 0], doe.get("X")[:, 1], color="black", alpha=0.3) - - for k, sols in enumerate(algorithm.trace_assigned): - if len(sols) > 0: - pop = Population.create(*sols) - plt.scatter(pop.get("X")[:, 0], pop.get("X")[:, 1], color="blue", alpha=0.3) - - x = algorithm.influenced[k].X - for sol in sols: - plt.plot((x[0], sol.X[0]), (x[1], sol.X[1]), alpha=0.1, color="black") - - plt.scatter(algorithm.influenced.get("X")[:, 0], algorithm.influenced.get("X")[:, 1], color="red", marker="*", - alpha=0.7, - label="influenced") - - _biased = Population.create( - *[e for e in algorithm.biased if e is not None]) - plt.scatter(_biased.get("X")[:, 0], _biased.get("X")[:, 1], color="orange", marker="s", label="Selected", - alpha=0.8, - s=100) - - plt.legend() - - # ========================================================================================================= # Algorithm # ========================================================================================================= class GPSAF(SurrogateAssistedAlgorithm): - - def __init__(self, - algorithm, - alpha=5, - beta=30, - rho=None, - n_max_doe=100, - n_max_infills=np.inf, - **kwargs): + def __init__(self, algorithm, alpha=5, beta=30, rho=None, n_max_doe=100, n_max_infills=np.inf, **kwargs): SurrogateAssistedAlgorithm.__init__(self, **kwargs) @@ -163,10 +95,6 @@ def __init__(self, # the maximum number of infill solutions self.n_max_infills = n_max_infills - self.surr_infills = None - self.influenced, self.trace, self.biased = None, None, None - self.restart = False - def _setup(self, problem, **kwargs): super()._setup(problem, **kwargs) @@ -175,15 +103,12 @@ def _setup(self, problem, **kwargs): self.proto.termination = default_termination(problem) # setup the underlying algorithm - kwargs['seed'] = self.seed - self.proto.setup(problem, **kwargs) + kwargs["seed"] = self.seed + self.proto.setup(problem, **kwargs) # copy the algorithm object to get started self.algorithm = deepcopy(self.proto) - # customize the display to show the surrogate influence - # self.display = GPSAFOutput(self.algorithm.display) - # self.display = Display(output=self.algorithm.display.output) # customize the display to show the surrogate influence self.display = self.algorithm.display self.display.output = GPSAFOutput(self.algorithm.output) @@ -192,30 +117,17 @@ def _setup(self, problem, **kwargs): # define the survival for the individuals to keep self.survival = FitnessSurvival() if problem.n_obj == 1 else RankAndCrowdingSurvival() - def _initialize_infill(self): - # return self.algorithm.infill() - return super()._initialize_infill() - def _initialize_advance(self, infills=None, **kwargs): super()._initialize_advance(infills=infills, **kwargs) # validate and check different surrogates to find the best - self.surrogate.validate(infills) - - # now we perform a fake initialization of the algorithm object by providing individuals from LHS - # fake = self.algorithm.infill() - # fittest = self.survival.do(self.problem, infills, n_survive=len(fake)) - fittest = infills + self.surrogate.validate(infills, random_state=self.random_state) # feed back the fittest individuals to the algorithm - self.algorithm.advance(infills=fittest) + self.algorithm.advance(infills=infills) def _infill(self): - # if the algorithm should do a restart - copy again the prototype - if self.restart: - self.algorithm = deepcopy(self.proto) - # get the design of experiments to be used for modeling doe = self._doe() @@ -238,11 +150,9 @@ def _infill(self): # continue running the algorithm for more generations if beta is "enabled" if self.beta > 0: - # get the trace from the beta run on the surrogate trace = self._infill_beta() trace.set("type", "trace") - self.trace = trace # 3) assign the found solutions to the original infill solutions trace_assigned = self._infill_beta_assign(influenced, trace) @@ -252,7 +162,6 @@ def _infill(self): # for each solution from the surrogate based optimization for i, pool in enumerate(trace_assigned): - # if there are no solutions to replace continue directly if len(pool) == 0: continue @@ -268,29 +177,17 @@ def _infill(self): rho = (len(pool) / max([len(e) for e in trace_assigned])) ** 0.5 # if the solution should be replaced - if np.random.random() <= rho: - + if self.random_state.random() <= rho: # if it should be replaced find the ONE solution from the pool - biased = self._infill_prob_tourn(pool, method="tournament", error=error, n_winners=1)[0] + biased = self._infill_prob_tourn(pool, error=error, n_winners=1)[0] # check the distance to existing solutions closest = norm_eucl_dist(self.problem, biased.get("X"), self._archive.get("X")).min() # if the solution is in fact new if closest > eps: - # now actually set the value to the infills infills[i] = biased - # infills[i].X = biased.X - - else: - print("BIASED: TOO CLOSE (SKIP)") - - closest = norm_eucl_dist(self.problem, infills[i].get("X"), self._archive.get("X")).min() - - # if the solution is in fact new - if closest < eps: - print("INFLUENCED: TOO CLOSE") # if beta is zero, then simply take the results from the alpha phase else: @@ -315,25 +212,23 @@ def _infill_alpha(self, infills, error=None): # reduce the number of infill solutions if required if len(influenced) > self.n_max_infills: - influenced = self._infill_prob_tourn(influenced, method="tournament", error=error, n_winners=self.n_max_infills) + influenced = self._infill_prob_tourn(influenced, error=error, n_winners=self.n_max_infills) # do the tournament for each alpha - for k in range(self.alpha - 1): - + for _ in range(self.alpha - 1): # create a second pool and actually do the tournament others = self.algorithm.infill() if len(others) > len(influenced): - I = np.random.permutation(len(others))[:len(influenced)] + I = self.random_state.permutation(len(others))[: len(influenced)] others = others[I] Evaluator().eval(problem, others) # for each offspring see if we do the surrogate tournament - for k in range(len(influenced)): - + for i in range(len(influenced)): # if the competitor is not worse it will take the lead - if get_relation(influenced[k], others[k]) < 1: - influenced[k] = others[k] + if get_relation(influenced[i], others[i]) < 1: + influenced[i] = others[i] return influenced @@ -341,13 +236,11 @@ def _infill_beta_assign(self, influenced, trace, filter=False): ret = [[] for _ in range(len(influenced))] if len(trace) > 0: - # find the closest individuals for each candidate to offsprings (and NOT the current replaced one) dists = norm_eucl_dist(self.problem, trace.get("X"), influenced.get("X")) closest = dists.argmin(axis=1) for k, i in enumerate(closest): - # add the solution closest to the influence to the list if not filter or get_relation(influenced[i], trace[k]) < 1: trace[k].set("i", i) @@ -362,11 +255,6 @@ def _infill_beta(self): # create a copy of the algorithm object to keep the original unmodified algorithm = deepcopy(self.algorithm) - # if np.random.random() < 0.5: - # algorithm = deepcopy(self.algorithm) - # else: - # algorithm = deepcopy(self.proto) - # print("PROTO") # disable the termination to have enough iterations to continue algorithm.termination = NoTermination() @@ -380,7 +268,6 @@ def _infill_beta(self): # run the algorithm for beta iterations for k in range(self.beta): - # just to make sure no algorithm specific termination criterion has be executed if not algorithm.has_next(): break @@ -397,118 +284,83 @@ def _infill_beta(self): return Population.create(*trace) - def _infill_prob_tourn(self, sols, n_winners=1, method="tournament", error=None): - - # if the beta phase has not found any solutions close to the influenced one - if len(sols) == 0: - return None - - else: - - if method == "best": - return FitnessSurvival().do(self.problem, Population.create(*sols), n_survive=n_winners)[0] - - elif method == "random": - return np.random.choice(sols, size=n_winners) - - elif method == "tournament": - - # create a copy of all solutions to be considered - pool = list(range(len(sols))) - - # until we have found a clear winner of the tournament - while True: - - # always shuffle the pool to have random tournaments - random.shuffle(pool) - - # make sure the pool is an even number - if len(pool) % 2 != 0: - pool.append(np.random.choice(pool)) - - # create the pairs that compete with each other - pairs = np.reshape(np.array(pool), (-1, 2)) - - # prepare the next pool already containing all the winners - winners = [] - - # create a solution pool with noise - sols_with_noise = noisy(sols, error) - - for i, j in pairs: + def _infill_prob_tourn(self, sols, n_winners=1, error=None): + # a knockout tournament with probabilistic comparisons under surrogate-prediction noise + + # create a copy of all solutions to be considered + pool = list(range(len(sols))) + + # until we have found a clear winner of the tournament + while True: + # always shuffle the pool to have random tournaments + self.random_state.shuffle(pool) + + # make sure the pool is an even number + if len(pool) % 2 != 0: + pool.append(self.random_state.choice(pool)) + + # create the pairs that compete with each other + pairs = np.reshape(np.array(pool), (-1, 2)) + + # prepare the next pool already containing all the winners + winners = [] + + # create a solution pool with noise + sols_with_noise = noisy(sols, error, random_state=self.random_state) + + for i, j in pairs: + # the two solutions to be compared + a, b = sols_with_noise[i], sols_with_noise[j] + + # calc the relation in a probabilistic manner + rel = get_relation(a, b) + + if rel == 1: + winners.append(i) + elif rel == -1: + winners.append(j) + else: + if a.get("k") is not None and b.get("k") is not None: + k = compare( + i, + a.get("k"), + j, + b.get("k"), + method="larger_is_better", + return_random_if_equal=True, + random_state=self.random_state, + ) + else: + k = self.random_state.choice([i, j]) - # the two solutions to be compared - a, b = sols_with_noise[i], sols_with_noise[j] + winners.append(k) - # calc the relation in a probabilistic manner - rel = get_relation(a, b) + if len(winners) <= n_winners: + if len(winners) < n_winners: + H = set(winners) - if rel == 1: - winners.append(i) - elif rel == -1: - winners.append(j) - else: - if a.get("k") is not None and b.get("k") is not None: - k = compare(i, a.get("k"), j, b.get("k"), method='larger_is_better', - return_random_if_equal=True) - else: - k = np.random.choice([i, j]) + self.random_state.shuffle(pool) + for k in pool: + if k not in H: winners.append(k) + H.add(k) - if len(winners) <= n_winners: - - if len(winners) < n_winners: - H = set(winners) - - random.shuffle(pool) - - for k in pool: - - if k not in H: - winners.append(k) - H.add(k) + if len(winners) == n_winners: + break - if len(winners) == n_winners: - break + return sols[winners] - return sols[winners] - - pool = winners - - else: - raise Exception("Unknown selection.") + pool = winners def _advance(self, infills=None, **kwargs): - if self.restart: - - for k in np.random.permutation(len(infills)): - - opt = np.random.choice(self.opt) - if get_relation(opt, infills[k]) >= 0: - infills[k] = opt - break - - self.restart = False - - # validate the current model - self.surrogate.validate(trn=self.doe, tst=infills) + # re-select the surrogate model (lazily; see nth_validate) + self.revalidate(trn=self.doe, tst=infills) # make a step in the main algorithm with high-fidelity solutions self.algorithm.advance(infills=infills, **kwargs) - if not self.algorithm.has_next(): - # self.restart = True - # print("RESTART") - - self.restart = False - print("RESTART is DISABLED") - - # for target in self.surrogate.targets: - # print(target.label, target.best) - - super()._advance(infills=infills, **kwargs) def _doe(self): @@ -517,8 +369,11 @@ def _doe(self): doe = self._archive if len(doe) > n_max_doe: - - center = LHS().do(self.problem, n_max_doe).get("X") + # Thread the run's Generator into the LHS: without it, pymoo falls back to an unseeded + # default_rng(), so the cluster centers -- and therefore which archive points are kept + # for the surrogate once the archive exceeds n_max_doe -- were random each run. That was + # the sole source of GPSAF's run-to-run irreproducibility (it only bit past n_max_doe). + center = LHS().do(self.problem, n_max_doe, random_state=self.random_state).get("X") A = cdist(doe.get("X"), center).argmin(axis=1) cluster = [[] for _ in range(n_max_doe)] @@ -528,7 +383,7 @@ def _doe(self): doe = [] for c in cluster: if len(c) > 0: - s = np.random.choice(c) + s = self.random_state.choice(c) doe.append(s) doe = self._archive[doe] @@ -540,7 +395,6 @@ def _doe(self): # if we have constraints also select the closest infeas. or feas. solution others = [] if self.problem.has_constraints(): - is_feas = self._archive.get("feas") feas, infeas = np.where(is_feas)[0], np.where(~is_feas)[0] diff --git a/src/pysamoo/algorithms/krvea.py b/src/pysamoo/algorithms/krvea.py new file mode 100644 index 0000000..2497d21 --- /dev/null +++ b/src/pysamoo/algorithms/krvea.py @@ -0,0 +1,122 @@ +"""K-RVEA -- Kriging-assisted reference-vector guided EA for expensive many-objective optimization.""" + +import numpy as np +from pymoo.algorithms.moo.rvea import RVEA +from pymoo.core.population import Population +from pymoo.core.problem import Problem +from pymoo.optimize import minimize as pymoo_minimize +from pymoo.util.display.multi import MultiObjectiveOutput +from pymoo.util.ref_dirs import get_reference_directions + +from pysamoo.algorithms._ego import default_kriging, fit_per_objective, pareto_optimum +from pysamoo.core.algorithm import SurrogateAssistedAlgorithm + + +class _KrigingProblem(Problem): + """A cheap pymoo problem whose objectives are the per-objective Kriging *mean* predictions.""" + + def __init__(self, models, xl, xu): + super().__init__(n_var=len(xl), n_obj=len(models), xl=xl, xu=xu) + self.models = models + + def _evaluate(self, X, out, *args, **kwargs): + out["F"] = np.column_stack([m.predict(X).y[:, 0] for m in self.models]) + + +class KRVEA(SurrogateAssistedAlgorithm): + """K-RVEA (Chugh et al., 2018): RVEA driven by Kriging models, with an adaptive infill criterion. + + Each iteration fits one Kriging model per objective on the archive, runs **RVEA** on those + (cheap) surrogate predictions for ``w_max`` generations, then selects ``n_infills`` candidates to + evaluate on the true function. The selection alternates between two K-RVEA criteria, switched by + how much the set of *active reference vectors* changed since the last iteration: + + * **diversity** (many active vectors changed -> the front is still moving): pick the + best-aligned candidate for distinct reference vectors, spreading the search; + * **convergence/uncertainty** (stable): pick the candidates with the highest Kriging + uncertainty, improving the models where they are least sure. + + Reuses pymoo's RVEA + Das-Dennis reference vectors and the pysurrogate Kriging surrogate + (Kriging is required here because the uncertainty criterion needs a predictive ``sigma``). + + Args: + ref_dirs: Reference vectors; ``None`` builds a Das-Dennis set sized to ``n_obj``. + n_infills: True-function evaluations selected per iteration (``u`` in the paper). + w_max: RVEA generations run on the surrogate between infills. + delta: Fraction-of-reference-vectors change above which the diversity criterion is used. + surrogate: pysurrogate Kriging prototype per objective (default ``Kriging(Exponential())``). + """ + + # K-RVEA manages its own per-objective Kriging models -> skip the base build. + build_default_surrogate = False + + def __init__(self, ref_dirs=None, n_infills=5, w_max=20, delta=0.05, surrogate=None, output=None, **kwargs): + super().__init__(output=output if output is not None else MultiObjectiveOutput(), **kwargs) + self.ref_dirs = ref_dirs + self.n_infills = n_infills + self.w_max = w_max + self.delta = delta + self.surrogate_proto = surrogate if surrogate is not None else default_kriging() + self._active_prev = None + + def _setup(self, problem, **kwargs): + super()._setup(problem, **kwargs) + if self.ref_dirs is None: + n_partitions = {2: 99, 3: 12}.get(problem.n_obj, 6) + self.ref_dirs = get_reference_directions("das-dennis", problem.n_obj, n_partitions=n_partitions) + + def _infill(self): + X, F = self._archive.get("X", "F") + problem = self.problem + + # 1) one Kriging per objective + models = fit_per_objective(self.surrogate_proto, X, F) + + # 2) optimize the surrogate with RVEA for w_max generations (threaded seed -> reproducible) + surr = _KrigingProblem(models, problem.xl, problem.xu) + seed = int(self.random_state.integers(1, 2**31 - 1)) + res = pymoo_minimize(surr, RVEA(ref_dirs=self.ref_dirs), ("n_gen", self.w_max), seed=seed, verbose=False) + Xc, Fc = res.pop.get("X"), res.pop.get("F") + + # 3) predictive uncertainty (summed sigma) for each candidate + sigma = np.column_stack([model.predict(Xc, var=True).sigma[:, 0] for model in models]).sum(axis=1) + + # 4) associate candidates to reference vectors by acute angle (translated to the ideal point) + U = Fc - Fc.min(axis=0) + Un = U / np.maximum(np.linalg.norm(U, axis=1, keepdims=True), 1e-12) + cos = Un @ self.ref_dirs.T + assign = cos.argmax(axis=1) + active = set(np.unique(assign).tolist()) + + # 5) K-RVEA adaptive criterion: how much the active-reference-vector set changed decides + # whether to emphasize diversity (spread) or convergence (reduce uncertainty). + changed = 1.0 if self._active_prev is None else len(active ^ self._active_prev) / len(self.ref_dirs) + self._active_prev = active + + u = min(self.n_infills, len(Xc)) + if changed > self.delta: + # diversity: best-aligned candidate per distinct active reference vector + picks = [] + for rv in sorted(active): + members = np.where(assign == rv)[0] + picks.append(int(members[cos[members, rv].argmax()])) + if len(picks) >= u: + break + # if fewer active vectors than the batch size, top up with the most uncertain candidates + # so the iteration always spends its full evaluation budget (as the paper's strategy does). + sel = picks[:u] + if len(sel) < u: + for i in np.argsort(-sigma): + if int(i) not in sel: + sel.append(int(i)) + if len(sel) == u: + break + sel = np.array(sel, dtype=int) + else: + # convergence: the u candidates the models are least certain about + sel = np.argsort(-sigma)[:u] + + return Population.new(X=Xc[sel]) + + def _set_optimum(self): + self.opt = pareto_optimum(self._archive) diff --git a/src/pysamoo/algorithms/moead_ego.py b/src/pysamoo/algorithms/moead_ego.py new file mode 100644 index 0000000..4c58977 --- /dev/null +++ b/src/pysamoo/algorithms/moead_ego.py @@ -0,0 +1,78 @@ +"""MOEA/D-EGO -- decomposition-based efficient global optimization with a batch infill per iteration.""" + +import numpy as np +from pymoo.core.population import Population +from pymoo.util.display.multi import MultiObjectiveOutput +from pymoo.util.nds.non_dominated_sorting import NonDominatedSorting +from pymoo.util.ref_dirs import get_reference_directions + +from pysamoo.algorithms._ego import default_kriging, fit_per_objective, lhs_local_pool, pareto_optimum, predict_mu_sigma +from pysamoo.core.algorithm import SurrogateAssistedAlgorithm + + +class MOEADEGO(SurrogateAssistedAlgorithm): + """MOEA/D-EGO (Zhang et al., 2010): per-objective Kriging + a decomposition-based batch infill. + + Unlike ParEGO (which refits one scalar surrogate for a *single* random weight each iteration), + MOEA/D-EGO fits one Kriging model per objective *once* per iteration and reuses them across many + weight vectors, selecting a whole batch of ``n_infills`` points -- one per (spread) weight vector. + Each subproblem scores candidates by the Tchebycheff aggregation of an *optimistic* prediction + ``mu - kappa*sigma`` (a lower-confidence-bound acquisition on the decomposed subproblem), and the + best not-yet-chosen candidate is taken, giving a diverse batch. Reuses pymoo's Das-Dennis + reference vectors and the pysurrogate Kriging surrogate. + + Args: + ref_dirs: Weight vectors; ``None`` builds a Das-Dennis set sized to ``n_obj``. + n_infills: Points evaluated per iteration (one per selected weight vector). + kappa: Exploration weight of the optimistic ``mu - kappa*sigma`` prediction. + pool: LHS candidate-pool size (augmented with local perturbations of the current front). + surrogate: pysurrogate Kriging prototype per objective (default ``Kriging(Exponential())``). + """ + + # manages its own per-objective Kriging models -> skip the base default-surrogate build. + build_default_surrogate = False + + def __init__(self, ref_dirs=None, n_infills=5, kappa=2.0, pool=200, surrogate=None, output=None, **kwargs): + super().__init__(output=output if output is not None else MultiObjectiveOutput(), **kwargs) + self.ref_dirs = ref_dirs + self.n_infills = n_infills + self.kappa = kappa + self.pool = pool + self.surrogate_proto = surrogate if surrogate is not None else default_kriging() + + def _setup(self, problem, **kwargs): + super()._setup(problem, **kwargs) + if self.ref_dirs is None: + n_partitions = {2: 99, 3: 12}.get(problem.n_obj, 6) + self.ref_dirs = get_reference_directions("das-dennis", problem.n_obj, n_partitions=n_partitions) + + def _infill(self): + X, F = self._archive.get("X", "F") + problem = self.problem + rng = self.random_state + + # one Kriging per objective (fit once, reused across all subproblems) + models = fit_per_objective(self.surrogate_proto, X, F) + + # candidate pool: LHS + local perturbations of the current non-dominated designs + nds = NonDominatedSorting().do(F, only_non_dominated_front=True) + cand = lhs_local_pool(problem, X[nds], self.pool, rng) + + # optimistic per-objective prediction (lower-confidence bound) + mu, sigma = predict_mu_sigma(models, cand) + lcb = mu - self.kappa * sigma + z = lcb.min(axis=0) # ideal-point estimate on the optimistic prediction + + # one point per (spread) weight vector by minimum Tchebycheff aggregation -> a diverse batch + picks = np.linspace(0, len(self.ref_dirs) - 1, self.n_infills).round().astype(int) + chosen: list = [] + for w in self.ref_dirs[picks]: + g = (w * (lcb - z)).max(axis=1) + for i in np.argsort(g): + if int(i) not in chosen: + chosen.append(int(i)) + break + return Population.new(X=cand[chosen]) + + def _set_optimum(self): + self.opt = pareto_optimum(self._archive) diff --git a/src/pysamoo/algorithms/parego.py b/src/pysamoo/algorithms/parego.py new file mode 100644 index 0000000..e24e85f --- /dev/null +++ b/src/pysamoo/algorithms/parego.py @@ -0,0 +1,82 @@ +"""ParEGO -- Pareto-efficient global optimization via random augmented-Tchebycheff scalarization.""" + +from copy import deepcopy + +import numpy as np +from pymoo.core.population import Population +from pymoo.util.display.multi import MultiObjectiveOutput +from pymoo.util.ref_dirs import get_reference_directions + +from pysamoo.algorithms._ego import default_kriging, pareto_optimum +from pysamoo.core.algorithm import SurrogateAssistedAlgorithm +from pysamoo.experimental.acquisition import LogEI +from pysamoo.experimental.infill import GlobalEI +from pysamoo.experimental.optimizer import VectorizedGradientDescent + + +class ParEGO(SurrogateAssistedAlgorithm): + """ParEGO (Knowles, 2006): single-objective EGO on a per-iteration random scalarization. + + Each infill draws one weight vector ``lambda`` uniformly from a fixed Das-Dennis set, collapses + the (archive-normalized) objectives to a single value with the *augmented Tchebycheff* function + ``g = max_i(lambda_i * f_i) + rho * sum_i(lambda_i * f_i)``, fits one Kriging model to those + scalar values, and maximizes Expected Improvement over the box to pick the next point. Rotating + the weight each iteration spreads the search across the whole Pareto front while only ever + optimizing a *single-objective* surrogate -- so it reuses the repo's existing EGO machinery + wholesale (the pysurrogate Kriging surrogate and the ``experimental`` EI acquisition + optimizer). + + This is the smallest end-to-end multi-objective EGO on the current stack; EHVI/qNEHVI slot into + the same acquisition seam later. + + Args: + rho: Weight of the augmentation term in the Tchebycheff scalarization (Knowles uses 0.05). + surrogate: A pysurrogate ``Model`` for the scalarized value (default ``Kriging(Exponential())``, + the same surrogate the single-objective BO defaults to). Re-fit fresh every infill. + infill: The acquisition seam (default ``GlobalEI(VectorizedGradientDescent())``) -- any + ``Infill`` that maximizes ``acq_func`` over the box. + acq_func: The acquisition function on the scalar surrogate (default ``LogEI``). + """ + + # ParEGO manages its own single-objective (scalar) surrogate -> skip the base build. + build_default_surrogate = False + + def __init__(self, rho=0.05, surrogate=None, infill=None, acq_func=None, output=None, **kwargs): + super().__init__(output=output if output is not None else MultiObjectiveOutput(), **kwargs) + self.rho = rho + self.surrogate_proto = surrogate if surrogate is not None else default_kriging() + self.infill_strategy = infill if infill is not None else GlobalEI(VectorizedGradientDescent()) + self.acq_func = acq_func if acq_func is not None else LogEI() + self.weights = None + self._model = None + + def _setup(self, problem, **kwargs): + super()._setup(problem, **kwargs) + # a fixed Das-Dennis weight set; one is drawn at random per infill. The partition count is + # picked so the set is neither tiny nor huge for the common 2-/3-objective cases. + n_partitions = {2: 100, 3: 15}.get(problem.n_obj, 8) + self.weights = get_reference_directions("das-dennis", problem.n_obj, n_partitions=n_partitions) + + def _infill(self): + X, F = self._archive.get("X", "F") + + # normalize the objectives to [0, 1] with the current archive (so the ideal point is 0 and + # every objective contributes on the same scale to the scalarization) + z_min, z_max = F.min(axis=0), F.max(axis=0) + Fn = (F - z_min) / np.maximum(z_max - z_min, 1e-12) + + # draw one weight and collapse to a scalar via augmented Tchebycheff + lam = self.weights[self.random_state.integers(len(self.weights))] + d = lam * Fn + y = d.max(axis=1) + self.rho * d.sum(axis=1) + + # fit a fresh surrogate on the scalarized values (the scalarization changes every infill) + model = deepcopy(self.surrogate_proto) + model.fit(X, y) + self._model = model + + # maximize (Log)EI on the scalar surrogate to choose the next point + x_best, _ = self.infill_strategy.do(self.problem, lambda: model, X, y, self.acq_func, self.random_state) + return Population.new(X=x_best[None, :]) + + def _set_optimum(self): + self.opt = pareto_optimum(self._archive) diff --git a/pysamoo/algorithms/psaf.py b/src/pysamoo/algorithms/psaf.py similarity index 79% rename from pysamoo/algorithms/psaf.py rename to src/pysamoo/algorithms/psaf.py index a5e363d..2220a6a 100644 --- a/pysamoo/algorithms/psaf.py +++ b/src/pysamoo/algorithms/psaf.py @@ -1,9 +1,9 @@ +"""PSAF — probabilistic surrogate-assisted framework for single-objective problems.""" + from copy import deepcopy -import numpy as np from ezmodel.core.factory import models_from_clazzes from ezmodel.models.knn import KNN -from ezmodel.models.kriging import Kriging from ezmodel.models.rbf import RBF from pymoo.algorithms.moo.nsga2 import RankAndCrowdingSurvival from pymoo.algorithms.soo.nonconvex.ga import FitnessSurvival @@ -22,7 +22,6 @@ class PSAFOutput(SingleObjectiveOutput): - def __init__(self, output, **kwargs): super().__init__(**kwargs) self.output = output @@ -31,11 +30,11 @@ def __init__(self, output, **kwargs): self.r2 = Column(name="r2", func=lambda a: a.r2) self.only_one_mode = False self.mae = Column(name="mae") - self.model = Column(name="model", width=60) + self.model = Column(name="model", width=60) def initialize(self, algorithm): self.output.initialize(algorithm) - self.columns = [e for e in self.output.columns if e.name != 'f_avg'] + [self.r2, self.bias] + self.columns = [e for e in self.output.columns if e.name != "f_avg"] + [self.r2, self.bias] problem = algorithm.problem if problem.n_obj == 1 and problem.n_constr == 0: @@ -55,22 +54,26 @@ def update(self, algorithm): class PSAF(SurrogateAssistedAlgorithm): - - def __init__(self, algorithm, alpha=5, beta=30, rho=None, rho_max=0.7, eps=0.005, **kwargs): + def __init__(self, algorithm, alpha=5, beta=30, rho=None, rho_min=0.7, eps=0.005, **kwargs): SurrogateAssistedAlgorithm.__init__(self, **kwargs) self.algorithm = deepcopy(algorithm) self.alpha = alpha self.beta = beta self.eps = eps self.rho = rho - self.rho_max = rho_max + self.rho_min = rho_min self.bias = rho self.r2 = None def _setup(self, problem, **kwargs): - assert problem.n_obj == 1 and problem.n_constr == 0, "PSAF only works for unconstrained single-objective problems!" + assert problem.n_obj == 1 and problem.n_constr == 0, ( + "PSAF only works for unconstrained single-objective problems!" + ) super()._setup(problem, **kwargs) + # thread the run's seed into the inner algorithm so a seed passed via the constructor (not + # only via minimize(seed=)) is honoured -- matching GPSAF and keeping the inner GA reproducible. + kwargs["seed"] = self.seed self.algorithm.setup(problem, **kwargs) # customize the display to show the surrogate influence @@ -81,9 +84,12 @@ def _setup(self, problem, **kwargs): xl, xu = problem.bounds() defaults = dict(norm_X=ZeroToOneNormalization(xl, xu)) + # PSAF's model pool is the RBF family plus a KNN baseline. Two Kriging variants used to be + # added here with a string regr ("constant"/"linear"), which pydacefit silently failed to fit + # (dropped under the benchmark's raise_exception=False) -- so PSAF has always run RBF+KNN. + # Re-enabling Kriging with proper regression objects was measured to *regress* PSAF on the + # benchmark problems, so the pool intentionally stays RBF + baseline. models = models_from_clazzes(RBF, **defaults) - models = {name: entry["model"] for name, entry in models.items()} - models = {**models, **{"krg-cont": Kriging(regr="constant"), "krg-lin": Kriging(regr="linear")}} if "baseline" not in models: models["baseline"] = KNN(problem.n_var + 1) @@ -98,7 +104,7 @@ def _setup(self, problem, **kwargs): def _initialize_advance(self, infills=None, **kwargs): # validate and check different surrogates to find the best - self.surrogate.validate(infills, exclude=["baseline"]) + self.surrogate.validate(infills, exclude=["baseline"], random_state=self.random_state) # now we perform a fake initialization of the algorithm object by providing individuals from LHS fake = self.algorithm.infill() @@ -116,7 +122,12 @@ def _infill(self): # set the bias the surrogate is supposed to have - only if greater than zero we do the second phase target = surrogate.targets[0] r2 = 1 - (target.performance("mse") / target.performance("mse", model="baseline")) - bias = max(self.rho_max, r2) if self.rho is None else self.rho + # rho_min is a *floor* on the replacement probability: PSAF exploits the surrogate at a base + # rate rho_min and raises it toward 1 only when the surrogate is clearly better than the KNN + # baseline (r2 > rho_min). Empirically this floor is what makes PSAF converge far past the + # bare algorithm -- lowering the bias when the surrogate looks weak makes it no better than + # the baseline. (The name is rho_min, not rho_max, to reflect that it is the floor.) + bias = max(self.rho_min, r2) if self.rho is None else self.rho # calculate the infill solutions as the algorithms usually would off = algorithm.infill() @@ -126,30 +137,25 @@ def _infill(self): # if a tournament selection should be done alpha is at least two if self.alpha > 1: - # do the tournament for each alpha - for k in range(self.alpha - 1): - + for _ in range(self.alpha - 1): # create a second pool and actually do the tournament others = self.algorithm.infill() Evaluator().eval(problem, others) # for each offspring see if we do the surrogate tournament for k in range(len(off)): - # if the competitor is not worse it will take the lead if get_relation(others[k], off[k]) >= 0: off[k] = others[k] # if algorithm shall be continued on the surrogate and there is a bias at all if self.beta > 0 and bias > 0.0: - # already calculate what individuals will be replaced later - replace = np.random.random(len(off)) <= bias + replace = self.random_state.random(len(off)) <= bias # if at least one is replaced actually simulate the algorithm on the surrogate if replace.sum() > 0: - # create a copy of the algorithm object algorithm = deepcopy(self.algorithm) algorithm.termination = NoTermination() @@ -160,7 +166,6 @@ def _infill(self): # run the algorithm for beta generations and always assign replacement candidates for k in range(self.beta): - # just to make sure no algorithm specific termination criterion has be executed if not algorithm.has_next(): break @@ -171,14 +176,12 @@ def _infill(self): # if there is some candidates to consider if len(infills) > 0: - # find the closest individuals for each candidate to offsprings dists = norm_eucl_dist(problem, infills.get("X"), off.get("X")) I = dists.argmin(axis=1) # for each infill solution check if it replaces the candidate for j in range(len(infills)): - # get the index to the closest of offsprings i = I[j] @@ -191,13 +194,12 @@ def _infill(self): # now do the probabilistic replacement for i in range(len(off)): - # if it should be replaced (that has been pre-calculated) if replace[i]: off[i] = cands[i] self.bias = bias - self.r2 = r2 + self.r2 = r2 # no only use the X values infills = Population.new(X=off.get("X")) @@ -208,8 +210,8 @@ def _infill(self): return infills def _advance(self, infills=None, **kwargs): - # update the surrogate(s) with the new infills points - self.surrogate.validate(self._archive, infills, exclude=["baseline"]) + # re-select the surrogate model with the new infill points (lazily; see nth_validate) + self.revalidate(self._archive, infills, exclude=["baseline"]) # make a step in the main algorithm with high-fidelity solutions self.algorithm.advance(infills=infills, **kwargs) diff --git a/src/pysamoo/algorithms/saasbo.py b/src/pysamoo/algorithms/saasbo.py new file mode 100644 index 0000000..0ddba46 --- /dev/null +++ b/src/pysamoo/algorithms/saasbo.py @@ -0,0 +1,72 @@ +"""SAASBO -- Sparse Axis-Aligned Subspace Bayesian Optimization (a MAP / shrinkage approximation).""" + +from copy import deepcopy + +from pymoo.core.population import Population +from pymoo.util.display.single import SingleObjectiveOutput +from pysurrogate.dace import Exponential +from pysurrogate.models import Kriging + +from pysamoo.algorithms._ego import best_optimum +from pysamoo.core.algorithm import SurrogateAssistedAlgorithm +from pysamoo.experimental.acquisition import LogEI +from pysamoo.experimental.infill import GlobalEI +from pysamoo.experimental.optimizer import GeneticAlgorithm + + +class SAASBO(SurrogateAssistedAlgorithm): + """SAASBO (Eriksson & Jankowiak, 2021): Bayesian optimization with a *sparse axis-aligned* GP. + + High-dimensional BO fails because a full-ARD GP over-fits its per-dimension length-scales with few + samples. SAASBO puts a strong shrinkage prior on those length-scales so most dimensions stay + "off" (long length-scale) unless the data demands otherwise, concentrating the model on the few + active axes. The reference method samples the prior with NUTS; here we use the cheaper MAP form + the surrogate already supports -- an ARD Kriging model with a shrinkage ``theta_prior`` on the + (log) length-scales -- and drive it with standard Expected Improvement. Reuses the pysurrogate + ARD Kriging + shrinkage prior and the experimental EI acquisition. + + .. note:: + This is a **scaffold, not yet competitive**. Faithful SAASBO relies on a *sparse GP with NUTS + hyperparameter sampling*; the MAP ``theta_prior`` here is a coarse substitute and does not + reliably beat a plain baseline on high-dimensional problems. Making SAASBO competitive needs + the sparse-GP / MCMC infrastructure that ``pysurrogate`` does not yet have -- so this class is + shipped as the algorithm structure (runnable and reproducible) with that modeling work called + out, and it carries no performance assertion until the surrogate side lands. + + Args: + theta_prior: ``(mean, std)`` shrinkage prior on the log length-scale hyperparameters; a small + mean/std pulls dimensions toward "inactive" (the sparsity that makes ARD work in high-d). + surrogate: pysurrogate surrogate (default: ARD ``Kriging(Exponential())`` with ``theta_prior``). + infill: acquisition seam (default ``GlobalEI(GeneticAlgorithm())`` -- derivative-free, robust). + acq_func: acquisition function (default ``LogEI``). + """ + + # manages its own sparse-ARD Kriging model -> skip the base default-surrogate build. + build_default_surrogate = False + + def __init__(self, theta_prior=(0.0, 0.01), surrogate=None, infill=None, acq_func=None, output=None, **kwargs): + super().__init__(output=output if output is not None else SingleObjectiveOutput(), **kwargs) + self.theta_prior = theta_prior + if surrogate is not None: + self.surrogate_proto = surrogate + else: + self.surrogate_proto = Kriging(corr=Exponential(), ARD=True, theta_prior=theta_prior) + self.infill_strategy = infill if infill is not None else GlobalEI(GeneticAlgorithm()) + self.acq_func = acq_func if acq_func is not None else LogEI() + self._model = None + + def _infill(self): + X, F = self._archive.get("X", "F") + y = F[:, 0] + + # fit the sparse-ARD surrogate on all data + model = deepcopy(self.surrogate_proto) + model.fit(X, y) + self._model = model + + # maximize Expected Improvement over the box + x_best, _ = self.infill_strategy.do(self.problem, lambda: model, X, y, self.acq_func, self.random_state) + return Population.new(X=x_best[None, :]) + + def _set_optimum(self): + self.opt = best_optimum(self.problem, self._archive) diff --git a/src/pysamoo/algorithms/ssansga2.py b/src/pysamoo/algorithms/ssansga2.py new file mode 100644 index 0000000..fd68d98 --- /dev/null +++ b/src/pysamoo/algorithms/ssansga2.py @@ -0,0 +1,107 @@ +"""SSANSGA2 — steady-state surrogate-assisted NSGA-II.""" + +from pymoo.algorithms.moo.nsga2 import NSGA2 +from pymoo.core.duplicate import DefaultDuplicateElimination +from pymoo.core.population import Population +from pymoo.optimize import minimize +from pymoo.util.display.multi import MultiObjectiveOutput +from pymoo.util.nds.non_dominated_sorting import NonDominatedSorting +from pymoo.util.normalization import normalize +from pymoo.util.roulette import RouletteWheelSelection +from sklearn.cluster import KMeans + +from pysamoo.core.algorithm import SurrogateAssistedAlgorithm + + +class SSANSGA2(SurrogateAssistedAlgorithm): + def __init__( + self, + n_infills=10, + surr_pop_size=100, + # Only 20 inner generations on purpose: running the inner NSGA2 to deep convergence + # over-exploits an inaccurate surrogate and drives the search into a wrong region (the + # bimodal "stall" some seeds showed). Stopping the inner search early keeps infills near + # the *reliable* part of the surrogate and is both more robust and faster (see the + # benchmark: ZDT1 worst-case IGD 0.99 -> 0.04 vs gen=50, ~17% less wall-time). + surr_n_gen=20, + surr_eps_elim=1e-6, + surr_sampling="current", + output=None, + **kwargs, + ): + + super().__init__(output=output if output is not None else MultiObjectiveOutput(), **kwargs) + self.n_infills = n_infills + self.surr_n_gen = surr_n_gen + self.surr_pop_size = surr_pop_size + self.surr_eps_elim = surr_eps_elim + self.surr_sampling = surr_sampling + + def _initialize_advance(self, infills=None, **kwargs): + super()._initialize_advance(infills, **kwargs) + self.surrogate.validate(infills, random_state=self.random_state) + + def _infill(self): + + self.surrogate.fit(self._archive) + + problem = self.surrogate.problem() + + if self.surr_sampling == "current": + sampling = self._archive + elif self.surr_sampling == "random": + sampling = None + else: + raise Exception("Unknown surrogate sampling strategy.") + + algorithm = NSGA2(pop_size=self.surr_pop_size, sampling=sampling) + + # Thread the run's Generator into the inner search instead of a hard-coded seed=1: a fixed + # seed made every infill's inner NSGA2 explore the same way, correlating the infills across + # iterations and hurting diversity. Deriving the seed from self.random_state keeps runs + # reproducible for a fixed outer seed while diversifying the inner search each iteration. + inner_seed = int(self.random_state.integers(1, 2**31 - 1)) + res = minimize(problem, algorithm, ("n_gen", self.surr_n_gen), seed=inner_seed, verbose=False) + + cand = DefaultDuplicateElimination(epsilon=self.surr_eps_elim).do(res.pop, self._archive) + + if len(cand) <= self.n_infills: + infills = Population.new(X=cand.get("X")) + + else: + ideal = res.opt.get("F").min(axis=0) + nadir = res.opt.get("F").max(axis=0) + 1e-16 + vals = normalize(cand.get("F"), ideal, nadir) + + # derive the KMeans seed from the run RNG (not a fixed 0) so the clustering varies per + # iteration -- consistent with the inner-NSGA2 seeding above -- yet stays reproducible. + kmeans_seed = int(self.random_state.integers(1, 2**31 - 1)) + kmeans = KMeans(n_clusters=self.n_infills, random_state=kmeans_seed).fit(vals) + groups = [[] for _ in range(self.n_infills)] + for k, i in enumerate(kmeans.labels_): + groups[i].append(k) + + S = [] + + for group in groups: + if len(group) > 0: + # Prefer the most diverse (largest crowding) solution in each cluster. Crowding is + # +inf for boundary points, which would break roulette selection; ranking via a + # double-argsort tames the inf while preserving order (a single argsort gives a + # scrambled permutation, not a rank -- the original defect that made this pick random). + rank = cand[group].get("crowding").argsort().argsort().astype(float) + selection = RouletteWheelSelection(rank, larger_is_better=True) + I = group[selection.next(random_state=self.random_state)] + S.append(I) + + infills = Population.new(X=cand[S].get("X")) + + return infills + + def _advance(self, infills=None, **kwargs): + self.revalidate(self._archive, infills) + super()._advance(infills, **kwargs) + + def _set_optimum(self): + nds = NonDominatedSorting().do(self._archive.get("F"), only_non_dominated_front=True) + self.opt = self._archive[nds] diff --git a/src/pysamoo/algorithms/tsemo.py b/src/pysamoo/algorithms/tsemo.py new file mode 100644 index 0000000..9fdf923 --- /dev/null +++ b/src/pysamoo/algorithms/tsemo.py @@ -0,0 +1,85 @@ +"""TSEMO -- Thompson-Sampling Efficient Multi-objective Optimization.""" + +import numpy as np +from pymoo.core.population import Population +from pymoo.util.display.multi import MultiObjectiveOutput +from pymoo.util.nds.non_dominated_sorting import NonDominatedSorting + +from pysamoo.algorithms._ego import ( + default_kriging, + fit_per_objective, + front_and_hv, + lhs_local_pool, + pareto_optimum, + predict_mu_sigma, +) +from pysamoo.core.algorithm import SurrogateAssistedAlgorithm + + +class TSEMO(SurrogateAssistedAlgorithm): + """TSEMO (Bradford et al., 2018): Thompson-sampling multi-objective Bayesian optimization. + + Fits one Kriging model per objective, draws a single Thompson sample from each posterior over a + candidate pool, and takes the sample's Pareto-optimal candidates -- the search commits to *one* + plausible objective landscape rather than an average, which naturally balances exploration and + exploitation. From those sampled-Pareto candidates it greedily selects the ``n_infills`` points + that most increase the *true* front's hypervolume. Reuses pymoo's HV indicator + non-dominated + sorting and the pysurrogate Kriging surrogate (the ``sigma`` drives the Thompson draw). + + Args: + n_infills: True-function evaluations selected per iteration. + pool: LHS candidate-pool size (augmented with local perturbations of the current front). + surrogate: pysurrogate Kriging prototype per objective (default ``Kriging(Exponential())``). + """ + + # manages its own per-objective Kriging models -> skip the base default-surrogate build. + build_default_surrogate = False + + def __init__(self, n_infills=5, pool=200, surrogate=None, output=None, **kwargs): + super().__init__(output=output if output is not None else MultiObjectiveOutput(), **kwargs) + self.n_infills = n_infills + self.pool = pool + self.surrogate_proto = surrogate if surrogate is not None else default_kriging() + + def _infill(self): + X, F = self._archive.get("X", "F") + problem = self.problem + rng = self.random_state + + # one Kriging per objective + models = fit_per_objective(self.surrogate_proto, X, F) + + # current front, reference point and hypervolume + nds, front, hv = front_and_hv(F) + + # candidate pool: LHS + local perturbations of the current non-dominated designs + cand = lhs_local_pool(problem, X[nds], self.pool, rng) + + # Thompson sample: one posterior draw per objective at the candidates + mu, sigma = predict_mu_sigma(models, cand) + sample = mu + sigma * rng.standard_normal(mu.shape) + + # candidates that are Pareto-optimal under the sampled landscape + s_nd = NonDominatedSorting().do(sample, only_non_dominated_front=True) + + # greedily pick n_infills of them by hypervolume improvement (on the sample) over the true front + pool_idx, chosen = list(s_nd), [] + cur = front + for _ in range(min(self.n_infills, len(pool_idx))): + best_i, best_gain = None, -np.inf + hv_cur = float(hv(cur)) + for i in pool_idx: + gain = float(hv(np.vstack([cur, sample[i]]))) - hv_cur + if gain > best_gain: + best_gain, best_i = gain, i + chosen.append(best_i) + pool_idx.remove(best_i) + cur = np.vstack([cur, sample[best_i]]) + + # fall back to the best sampled candidates if none improved the hypervolume + if not chosen: + chosen = list(s_nd[: self.n_infills]) + return Population.new(X=cand[chosen]) + + def _set_optimum(self): + self.opt = pareto_optimum(self._archive) diff --git a/src/pysamoo/algorithms/turbo.py b/src/pysamoo/algorithms/turbo.py new file mode 100644 index 0000000..4a43323 --- /dev/null +++ b/src/pysamoo/algorithms/turbo.py @@ -0,0 +1,136 @@ +"""TuRBO -- Trust-Region Bayesian Optimization for higher-dimensional single-objective problems.""" + +from copy import deepcopy + +import numpy as np +from pymoo.core.population import Population +from pymoo.util.display.single import SingleObjectiveOutput + +from pysamoo.algorithms._ego import best_optimum, default_kriging +from pysamoo.core.algorithm import SurrogateAssistedAlgorithm +from pysamoo.experimental.acquisition import LogEI + + +class TuRBO(SurrogateAssistedAlgorithm): + """TuRBO-1 (Eriksson et al., 2019): Bayesian optimization inside an adaptive trust region. + + Standard global BO over-explores in higher dimensions; TuRBO restricts the surrogate and the + acquisition to a hyper-rectangular *trust region* centered on the incumbent. Each infill fits a + Kriging model, generates candidates inside the trust region (perturbing only a random subset of + coordinates per candidate, as in TuRBO), and picks the one with the best (Log)Expected + Improvement. The trust-region side length ``L`` adapts to progress: it doubles after ``succ_tol`` + consecutive improvements and halves after ``fail_tol`` failures; when it collapses below + ``length_min`` the region restarts at ``length_init``. Reuses the pysurrogate Kriging surrogate + and the experimental ``LogEI`` acquisition. + + Args: + n_candidates: Trust-region candidates scored per infill (default ``min(1000, 100*d)``). + length_init: Initial trust-region side as a fraction of the box width. + length_min: Side below which the trust region restarts. + length_max: Maximum trust-region side. + succ_tol: Consecutive improvements that grow the region. + fail_tol: Consecutive failures that shrink it (default ``max(4, d)``). + surrogate: pysurrogate Kriging prototype (default ``Kriging(Exponential())``). + acq_func: Acquisition scored over the trust-region candidates (default ``LogEI``). + """ + + # manages its own single-objective Kriging -> skip the base default-surrogate build. + build_default_surrogate = False + + def __init__( + self, + n_candidates=None, + length_init=0.8, + length_min=0.5**7, + length_max=1.6, + succ_tol=3, + fail_tol=None, + surrogate=None, + acq_func=None, + output=None, + **kwargs, + ): + super().__init__(output=output if output is not None else SingleObjectiveOutput(), **kwargs) + self.n_candidates = n_candidates + self.length_init = length_init + self.length_min = length_min + self.length_max = length_max + self.succ_tol = succ_tol + self.fail_tol = fail_tol + self.surrogate_proto = surrogate if surrogate is not None else default_kriging() + self.acq_func = acq_func if acq_func is not None else LogEI() + self.L = length_init + self.success = 0 + self.failure = 0 + self._restart = False + self._model = None + + def _setup(self, problem, **kwargs): + super()._setup(problem, **kwargs) + if self.n_candidates is None: + self.n_candidates = min(1000, 100 * problem.n_var) + if self.fail_tol is None: + self.fail_tol = max(4, problem.n_var) + + def _infill(self): + X, F = self._archive.get("X", "F") + y = F[:, 0] + problem = self.problem + xl, xu, d = problem.xl, problem.xu, problem.n_var + span = xu - xl + rng = self.random_state + + # restart: the trust region collapsed, so re-seed the search from a fresh random region + # instead of shrinking forever around a stuck incumbent. + if self._restart: + self._restart = False + self.L = self.length_init + return Population.new(X=(xl + rng.random(d) * span)[None, :]) + + # fit the surrogate on all data + model = deepcopy(self.surrogate_proto) + model.fit(X, y) + self._model = model + + # trust region (side L * box width) centered on the incumbent + x_center = X[int(y.argmin())] + half = 0.5 * self.L * span + tr_lb = np.maximum(x_center - half, xl) + tr_ub = np.minimum(x_center + half, xu) + + # candidates inside the trust region: each perturbs only a random subset of coordinates + n = self.n_candidates + pert = tr_lb + (tr_ub - tr_lb) * rng.random((n, d)) + prob = min(20.0 / d, 1.0) + mask = rng.random((n, d)) < prob + rows = np.where(~mask.any(axis=1))[0] # every candidate must perturb at least one coordinate + if len(rows): + mask[rows, rng.integers(0, d, size=len(rows))] = True + cand = np.repeat(x_center[None, :], n, axis=0).copy() + cand[mask] = pert[mask] + + # score by (Log)EI over the candidate set; the acquisition is in minimization form + pred = model.predict(cand, var=True) + acq = self.acq_func.calc(pred.y[:, 0], pred.sigma[:, 0], f_min=float(y.min())) + x_best = cand[int(np.argmin(acq))] + return Population.new(X=x_best[None, :]) + + def _advance(self, infills=None, **kwargs): + prev = float(self._archive.get("F")[:, 0].min()) if len(self._archive) else float("inf") + super()._advance(infills, **kwargs) + cur = float(self._archive.get("F")[:, 0].min()) + + if cur < prev - 1e-12 * max(1.0, abs(prev)): + self.success, self.failure = self.success + 1, 0 + else: + self.success, self.failure = 0, self.failure + 1 + + if self.success >= self.succ_tol: + self.L, self.success = min(2.0 * self.L, self.length_max), 0 + if self.failure >= self.fail_tol: + self.L, self.failure = self.L / 2.0, 0 + if self.L < self.length_min: # trust region collapsed -> restart from a fresh region next infill + self._restart, self.success, self.failure = True, 0, 0 + + def _set_optimum(self): + self.opt = best_optimum(self.problem, self._archive) diff --git a/src/pysamoo/benchmark/__init__.py b/src/pysamoo/benchmark/__init__.py new file mode 100644 index 0000000..6fac1e8 --- /dev/null +++ b/src/pysamoo/benchmark/__init__.py @@ -0,0 +1,25 @@ +"""Benchmarking harness for developing and comparing surrogate-assisted methods.""" + +from pysamoo.benchmark.core import ( + ProblemSpec, + Record, + Scenario, + Summary, + format_table, + run_benchmark, + score_run, + summarize, +) +from pysamoo.benchmark.models import make_surrogate + +__all__ = [ + "ProblemSpec", + "Record", + "Scenario", + "Summary", + "format_table", + "make_surrogate", + "run_benchmark", + "score_run", + "summarize", +] diff --git a/src/pysamoo/benchmark/core.py b/src/pysamoo/benchmark/core.py new file mode 100644 index 0000000..a030fc5 --- /dev/null +++ b/src/pysamoo/benchmark/core.py @@ -0,0 +1,212 @@ +"""Benchmark runner for comparing surrogate-assisted algorithms and models. + +The goal is to make it cheap to answer "is algorithm/surrogate A better than B +under a small evaluation budget?". You describe *scenarios* (named factories that +each build a fresh algorithm) and *problem specs* (a problem + an evaluation +budget), and :func:`run_benchmark` runs every scenario on every problem across +several seeds, scoring each run with a single "lower-is-better" metric: + +* single-objective problems -> best feasible objective value (the gap to the + known optimum when the problem exposes one); +* multi-objective problems -> IGD against the problem's Pareto front, or + hypervolume distance from a reference point when no front is available. + +Because :class:`~pysamoo.core.algorithm.SurrogateAssistedAlgorithm` accepts a +pluggable ``surrogate=`` object, the very same harness compares *surrogate +models* — see :func:`pysamoo.benchmark.models.make_surrogate`. +""" + +import time +from dataclasses import dataclass, field + +import numpy as np +from pymoo.indicators.hv import HV +from pymoo.indicators.igd import IGD +from pymoo.optimize import minimize + + +@dataclass +class Scenario: + """A named, repeatable way to build an algorithm. + + Args: + name: Label shown in result tables. + factory: Zero-argument callable returning a *fresh* algorithm instance. + It must build a new object on every call so seeds/runs stay isolated. + """ + + name: str + factory: object # Callable[[], Algorithm] + + +@dataclass +class ProblemSpec: + """A benchmark problem paired with its evaluation budget. + + Args: + name: Label shown in result tables. + problem: A pymoo ``Problem`` instance. + n_evals: Total number of true function evaluations allowed per run. + ref_point: Reference point for hypervolume when no Pareto front exists. + """ + + name: str + problem: object + n_evals: int + ref_point: object = None + + +@dataclass +class Record: + """One scored run of a scenario on a problem with a single seed.""" + + scenario: str + problem: str + seed: int + score: float + metric: str + n_eval: int + runtime: float + feasible: bool + + +@dataclass +class Summary: + """Aggregated statistics for a (scenario, problem) pair over all seeds.""" + + scenario: str + problem: str + metric: str + mean: float + std: float + best: float + mean_runtime: float + feasible_rate: float + n_runs: int + raw: list = field(default_factory=list) + + +def score_run(problem, res): + """Score a finished run with a single lower-is-better metric. + + Returns: + A ``(metric_name, value, feasible)`` tuple. ``value`` is ``inf`` and + ``feasible`` is ``False`` when the run produced no feasible solution. + """ + if problem.n_obj == 1: + F = None if res.F is None else np.atleast_1d(np.asarray(res.F, dtype=float)).ravel() + if F is None or F.size == 0 or not np.isfinite(F).all(): + return "f_gap", float("inf"), False + pf = _safe_pf(problem) + ideal = float(pf.min()) if pf is not None else 0.0 + return "f_gap", float(F.min()) - ideal, True + + # multi-objective + F = None if res.F is None else np.atleast_2d(np.asarray(res.F, dtype=float)) + if F is None or F.size == 0 or not np.isfinite(F).all(): + return "igd", float("inf"), False + pf = _safe_pf(problem) + if pf is not None: + return "igd", float(IGD(pf)(F)), True + ref = res_ref_point(problem, F) + return "hv_gap", float(_hv_gap(F, ref)), True + + +def _safe_pf(problem): + try: + pf = problem.pareto_front() + except Exception: + return None + if pf is None: + return None + return np.atleast_2d(np.asarray(pf, dtype=float)) + + +def res_ref_point(problem, F): + """A reference point worse than every observed objective vector.""" + return F.max(axis=0) + 1.0 + + +def _hv_gap(F, ref): + """Hypervolume turned into a lower-is-better score (negative hypervolume).""" + return -float(HV(ref_point=ref)(F)) + + +def run_benchmark(scenarios, problems, n_seeds=5, seed0=1, verbose=True): + """Run every scenario on every problem across ``n_seeds`` seeds. + + Args: + scenarios: Iterable of :class:`Scenario`. + problems: Iterable of :class:`ProblemSpec`. + n_seeds: Number of independent seeds per (scenario, problem) pair. + seed0: First seed; subsequent seeds are ``seed0 + i``. + verbose: When ``True``, print one line per finished run. + + Returns: + A list of :class:`Record`, one per run. + """ + records = [] + for spec in problems: + for scn in scenarios: + for i in range(n_seeds): + seed = seed0 + i + algorithm = scn.factory() + t0 = time.perf_counter() + res = minimize(spec.problem, algorithm, ("n_evals", spec.n_evals), seed=seed, verbose=False) + runtime = time.perf_counter() - t0 + metric, score, feasible = score_run(spec.problem, res) + n_eval = ( + int(getattr(getattr(res, "algorithm", None), "evaluator", None).n_eval) + if res.algorithm + else spec.n_evals + ) + rec = Record(scn.name, spec.name, seed, score, metric, n_eval, runtime, feasible) + records.append(rec) + if verbose: + print( + f"{spec.name:12s} {scn.name:16s} seed={seed} " + f"{metric}={score:.4g} t={runtime:.1f}s feasible={feasible}", + flush=True, + ) + return records + + +def summarize(records): + """Aggregate raw records into one :class:`Summary` per (scenario, problem).""" + groups = {} + for r in records: + groups.setdefault((r.problem, r.scenario), []).append(r) + + summaries = [] + for (problem, scenario), recs in groups.items(): + scores = np.array([r.score for r in recs], dtype=float) + finite = scores[np.isfinite(scores)] + runtimes = np.array([r.runtime for r in recs], dtype=float) + summaries.append( + Summary( + scenario=scenario, + problem=problem, + metric=recs[0].metric, + mean=float(finite.mean()) if finite.size else float("inf"), + std=float(finite.std()) if finite.size else float("nan"), + best=float(finite.min()) if finite.size else float("inf"), + mean_runtime=float(runtimes.mean()), + feasible_rate=float(np.mean([r.feasible for r in recs])), + n_runs=len(recs), + raw=recs, + ) + ) + return summaries + + +def format_table(summaries): + """Render summaries as an aligned, sorted text table (best mean first).""" + rows = sorted(summaries, key=lambda s: (s.problem, s.mean)) + header = f"{'problem':12s} {'scenario':16s} {'metric':7s} {'mean':>10s} {'std':>9s} {'best':>10s} {'time/run':>9s} {'feas':>5s}" + lines = [header, "-" * len(header)] + for s in rows: + lines.append( + f"{s.problem:12s} {s.scenario:16s} {s.metric:7s} " + f"{s.mean:10.4g} {s.std:9.3g} {s.best:10.4g} {s.mean_runtime:8.1f}s {s.feasible_rate:5.0%}" + ) + return "\n".join(lines) diff --git a/src/pysamoo/benchmark/models.py b/src/pysamoo/benchmark/models.py new file mode 100644 index 0000000..36d0edf --- /dev/null +++ b/src/pysamoo/benchmark/models.py @@ -0,0 +1,57 @@ +"""Helpers for plugging custom surrogate models into the benchmark. + +pysamoo's surrogate-assisted algorithms are *generic*: they only need a +:class:`~pysamoo.core.surrogate.Surrogate` describing how to model each problem +output. :func:`make_surrogate` builds that object from per-output model-set +factories, so swapping the model family is a one-liner. Each model-set factory +takes keyword defaults (e.g. ``norm_X``) and returns a ``{name: model}`` dict; +the surrogate cross-validates and selects among them automatically. + +Example: + >>> from ezmodel.models.rbf import RBF + >>> def only_rbf(**defaults): + ... return {"rbf-cubic": RBF(kernel="cubic", **defaults)} + >>> surrogate = make_surrogate(problem, obj_models=only_rbf) + >>> algorithm = GPSAF(NSGA2(), surrogate=surrogate) +""" + +from pysamoo.core.algorithm import MyNormalization +from pysamoo.core.defaults import ( + DEFAULT_EQ_CONSTR_MODELS, + DEFAULT_IEQ_CONSTR_MODELS, + DEFAULT_OBJ_MODELS, +) +from pysamoo.core.surrogate import Surrogate +from pysamoo.core.target import Target + + +def make_surrogate( + problem, + obj_models=DEFAULT_OBJ_MODELS, + ieq_models=DEFAULT_IEQ_CONSTR_MODELS, + eq_models=DEFAULT_EQ_CONSTR_MODELS, +): + """Build a :class:`Surrogate` for ``problem`` from per-output model factories. + + Args: + problem: The optimization problem (used only for metadata and bounds). + obj_models: Factory returning a ``{name: model}`` dict for each objective. + ieq_models: Factory for each inequality constraint. + eq_models: Factory for each equality constraint. + + Returns: + A :class:`~pysamoo.core.surrogate.Surrogate` ready to pass as the + ``surrogate=`` argument of a surrogate-assisted algorithm. + """ + xl, xu = problem.bounds() + defaults = dict(norm_X=MyNormalization(xl, xu)) + + targets = [] + for m in range(problem.n_obj): + targets.append(Target(("F", m), obj_models(**defaults))) + for g in range(problem.n_ieq_constr): + targets.append(Target(("G", g), ieq_models(**defaults))) + for h in range(problem.n_eq_constr): + targets.append(Target(("H", h), eq_models(**defaults))) + + return Surrogate(problem, targets) diff --git a/src/pysamoo/core/__init__.py b/src/pysamoo/core/__init__.py new file mode 100644 index 0000000..2fcfe5a --- /dev/null +++ b/src/pysamoo/core/__init__.py @@ -0,0 +1 @@ +"""Core building blocks shared by the surrogate-assisted algorithms.""" diff --git a/src/pysamoo/core/algorithm.py b/src/pysamoo/core/algorithm.py new file mode 100644 index 0000000..16b23b1 --- /dev/null +++ b/src/pysamoo/core/algorithm.py @@ -0,0 +1,168 @@ +"""Base class and helpers for surrogate-assisted algorithms.""" + +from pymoo.core.algorithm import Algorithm +from pymoo.core.initialization import Initialization +from pymoo.core.population import Population +from pymoo.operators.sampling.lhs import LHS +from pymoo.util.normalization import ZeroToOneNormalization + +from pysamoo.core.defaults import DEFAULT_EQ_CONSTR_MODELS, DEFAULT_IEQ_CONSTR_MODELS, DEFAULT_OBJ_MODELS +from pysamoo.core.selection import resolve as resolve_selection +from pysamoo.core.surrogate import Surrogate + + +def default_n_doe(n_var, cap=float("inf")): + """Default initial design-of-experiments size for ``n_var`` variables, optionally capped. + + Args: + n_var: Number of decision variables. + cap: Upper bound on the returned size (defaults to no cap). + + Returns: + ``min(2 * n_var + 1, cap)``. + """ + return min(2 * n_var + 1, cap) + + +class SurrogateAssistedAlgorithm(Algorithm): + # whether _setup builds the default multi-target model pool; the EGO-style algorithms that + # manage their own per-objective models set this to False (and skip the expensive build). + build_default_surrogate = True + + def __init__( + self, + n_initial_doe=None, + n_initial_max_doe=100, + sampling=None, + nth_validate=5, + surrogate=None, + selection="full", + **kwargs, + ): + """Base surrogate-assisted algorithm. + + Args: + n_initial_doe: Number of initial design-of-experiments points. If ``None``, defaults to + ``2 * n_var + 1`` (capped at ``n_initial_max_doe``). + n_initial_max_doe: Upper bound on the initial DOE size when ``n_initial_doe`` is ``None``. + sampling: The sampling operator used to generate the initial designs. + nth_validate: Re-run full model selection only every nth call to :meth:`revalidate` + (the model is still refit every iteration in between). + surrogate: A pre-built :class:`~pysamoo.core.surrogate.Surrogate`. If ``None``, a default + model pool is constructed and ``selection`` chooses among it. + selection: Pluggable model-selection strategy for the default surrogate — a name registered + in :data:`pysamoo.core.selection.STRATEGIES` or a Target factory + ``(label, models) -> Target``. ``"full"`` cross-validates the whole pool every + iteration. Ignored if ``surrogate`` is given. + """ + super().__init__(**kwargs) + + self.selection = selection + self.n_initial_doe = n_initial_doe + self.n_initial_max_doe = n_initial_max_doe + self.initialization = Initialization(sampling if sampling is not None else LHS()) + + # all solutions that have been evaluated so far + self._archive = Population() + + # here always the most recent infill solutions are stored + self.infills = None + + # the model/surrogate to be used during optimization + self.surrogate = surrogate + + # each nth iteration when all surrogate models should be revalidated + self.nth_validate = nth_validate + + # counts calls to revalidate() so re-selection can run only every nth_validate-th time + self._revalidate_count = 0 + + def _setup(self, problem, **kwargs): + # set the number of DOE points initially + if self.n_initial_doe is None: + self.n_initial_doe = min(self.n_initial_max_doe, default_n_doe(problem.n_var)) + + # build the default multi-target surrogate unless the algorithm manages its own models + if self.build_default_surrogate and self.surrogate is None: + self.surrogate = self._build_default_surrogate(problem) + + def _build_default_surrogate(self, problem): + """Construct the default multi-target surrogate (one model pool per objective/constraint). + + Args: + problem: The problem whose objective/constraint counts and bounds shape the surrogate. + + Returns: + A :class:`~pysamoo.core.surrogate.Surrogate` with a selection-driven target per output. + """ + # the design space boundaries for the problem - used for normalization in the surrogate + xl, xu = problem.bounds() + defaults = dict(norm_X=MyNormalization(xl, xu)) + + # the model-selection strategy is pluggable: resolve it to a target + # factory (label, models) -> Target. "full" or any factory. + make_target = resolve_selection(self.selection) + + targets = [] + + models = DEFAULT_OBJ_MODELS(**defaults) + for m in range(problem.n_obj): + targets.append(make_target(("F", m), models)) + + models = DEFAULT_IEQ_CONSTR_MODELS(**defaults) + for g in range(problem.n_ieq_constr): + targets.append(make_target(("G", g), models)) + + models = DEFAULT_EQ_CONSTR_MODELS(**defaults) + for h in range(problem.n_eq_constr): + targets.append(make_target(("H", h), models)) + + return Surrogate(problem, targets) + + def revalidate(self, *args, **kwargs): + """Re-run model selection lazily. + + Cross-validating the whole candidate pool and re-picking the best model + (``surrogate.validate``) is the dominant cost, yet the winner rarely + changes from one infill to the next. ``nth_validate`` decouples how often + we *re-select* from how often we *refit*: the full selection runs only + every ``nth_validate``-th call; in between this is a no-op and the current + best model is reused (algorithms still ``surrogate.fit`` it on the new data + every iteration). ``nth_validate=1`` re-selects every iteration (original + behaviour); ``None``/``0`` is treated the same. + """ + self._revalidate_count += 1 + # validate on the first call and then every nth_validate-th call (so a + # caller whose first selection happens here — e.g. BO — is covered). + if not self.nth_validate or (self._revalidate_count - 1) % self.nth_validate == 0: + self.surrogate.validate(*args, **kwargs) + + def _initialize_infill(self): + # Thread the run's Generator into sampling so the initial DOE is + # reproducible (pymoo's samplers default to a fresh RNG otherwise). + self.infills = self.initialization.do( + self.problem, self.n_initial_doe, algorithm=self, random_state=self.random_state + ) + return self.infills + + def _initialize_advance(self, infills=None, **kwargs): + self.infills = infills + self._archive = Population.merge(self._archive, infills) + + def _advance(self, infills=None, **kwargs): + self.infills = infills + self._archive = Population.merge(self._archive, infills) + + +class MyNormalization(ZeroToOneNormalization): + """Map the design space to ``[-100, 100]`` (symmetric, wider range than [0, 1]). + + Surrogate kernels (RBF/Kriging) are better conditioned on this symmetric, wider range than on the + plain unit cube, so the default model pool normalizes design inputs through this before fitting. + """ + + def forward(self, X): + return super().forward(X) * 200 - 100 + + def backward(self, X): + return super().backward((X + 100) / 200) diff --git a/src/pysamoo/core/defaults.py b/src/pysamoo/core/defaults.py new file mode 100644 index 0000000..733945e --- /dev/null +++ b/src/pysamoo/core/defaults.py @@ -0,0 +1,70 @@ +"""Default surrogate-model factories for objectives and constraints.""" + +from ezmodel.core.factory import models_from_clazzes +from ezmodel.models.kriging import Kriging +from ezmodel.models.rbf import RBF +from ezmodel.util.transformation.plog import Plog +from pydacefit.regr import ConstantRegression, LinearRegression, QuadraticRegression +from pymoo.util.normalization import NoNormalization + + +def DEFAULT_OBJ_MODELS(**defaults): + + # models_from_clazzes returns {name: model_instance} + models = models_from_clazzes(RBF, **defaults) + + # pydacefit expects a regression *object* (not the string "constant"/"linear"/...). + # Passing strings silently fails the fit (caught by the benchmark's raise_exception=False), + # which used to drop the entire Kriging zoo and leave only RBF -- a large, silent quality loss. + models["kriging-const"] = Kriging(regr=ConstantRegression()) + models["kriging-lin"] = Kriging(regr=LinearRegression()) + models["kriging-quadr"] = Kriging(regr=QuadraticRegression()) + + models["kriging-const-ARD"] = Kriging(regr=ConstantRegression(), ARD=True) + models["kriging-lin-ARD"] = Kriging(regr=LinearRegression(), ARD=True) + models["kriging-quadr-ARD"] = Kriging(regr=QuadraticRegression(), ARD=True) + + return models + + +def DEFAULT_IEQ_CONSTR_MODELS(**defaults): + models = {} + + for kernel in ["cubic", "linear", "mq"]: + # Plog is a *target* transform for constraint violations (SACOBRA-style), so it belongs on + # norm_y -- keep the caller's design-space norm_X instead of overwriting it. + for label, norm in [("default", NoNormalization()), ("plog", Plog())]: + for normalized in [False, True]: + for tail in ["constant", "linear", "linear+quadratic"]: + params = dict(defaults) + params["kernel"] = kernel + params["norm_y"] = norm + params["normalized"] = normalized + params["tail"] = tail + + model = RBF(**params) + models[f"rbf-{kernel}-{tail}-{label}-{normalized}"] = model + + return models + + +def DEFAULT_EQ_CONSTR_MODELS(**defaults): + models = {} + + for kernel in ["cubic", "linear", "mq"]: + for normalized in [False, True]: + for tail in ["constant", "linear", "linear+quadratic"]: + params = dict(defaults) + params["kernel"] = kernel + params["normalized"] = normalized + params["tail"] = tail + params["optimize"] = False + + model = RBF(**params) + models[f"rbf-{kernel}-{tail}-{normalized}-False"] = model + + models["kriging-const"] = Kriging(regr=ConstantRegression()) + models["kriging-lin"] = Kriging(regr=LinearRegression()) + models["kriging-quadr"] = Kriging(regr=QuadraticRegression()) + + return models diff --git a/pysamoo/core/indicator.py b/src/pysamoo/core/indicator.py similarity index 77% rename from pysamoo/core/indicator.py rename to src/pysamoo/core/indicator.py index e71bc2c..4271115 100644 --- a/pysamoo/core/indicator.py +++ b/src/pysamoo/core/indicator.py @@ -1,3 +1,5 @@ +"""Surrogate-accuracy indicators (MSE, RMSE, MAE, R2, Kendall tau, ...).""" + import numpy as np from scipy.stats import rankdata @@ -17,17 +19,16 @@ def calc_mae(y_true, y_hat, **kwargs): def kendall_tau(y_true, y_hat, trn_y=None, **kwargs): assert trn_y is not None, "For kendall tau the ranking needs to be calculated which requires the training data!" - a = rankdata(np.concatenate([trn_y, y_true]), method='min') - b = rankdata(np.concatenate([trn_y, y_hat]), method='min') + a = rankdata(np.concatenate([trn_y, y_true]), method="min") + b = rankdata(np.concatenate([trn_y, y_hat]), method="min") n = len(a) i, j = np.meshgrid(np.arange(n), np.arange(n)) - ndisordered = np.logical_or(np.logical_and(a[i] < a[j], b[i] > b[j]), - np.logical_and(a[i] > a[j], b[i] < b[j])).sum() - - # ndisordered = ndisordered / (n * (n - 1)) + ndisordered = np.logical_or( + np.logical_and(a[i] < a[j], b[i] > b[j]), np.logical_and(a[i] > a[j], b[i] < b[j]) + ).sum() return ndisordered diff --git a/src/pysamoo/core/knockout.py b/src/pysamoo/core/knockout.py new file mode 100644 index 0000000..ca1f151 --- /dev/null +++ b/src/pysamoo/core/knockout.py @@ -0,0 +1,32 @@ +"""Additive-noise perturbation of a population's outputs for uncertainty-aware comparisons.""" + +import numpy as np +from pymoo.core.population import Population + +from pysamoo.core.tcv import TotalConstraintViolation + + +def noisy(sols, error, random_state=None): + """Return a copy of a population with Gaussian noise added to selected outputs. + + Used by GPSAF to model surrogate prediction uncertainty when comparing solutions. + + Args: + sols: The population to perturb (its X/F/G/H are copied, not mutated). + error: Mapping from ``(output_type, column)`` to the noise standard deviation. + random_state: A numpy ``Generator`` for the noise (falls back to the global RNG). + + Returns: + A new population with the perturbed outputs and refreshed constraint violation. + """ + rng = random_state if random_state is not None else np.random + out = {} + for type in ["X", "F", "G", "H"]: + out[type] = np.copy(sols.get(type)) + + for (type, k), std in error.items(): + out[type][:, k] += rng.normal(loc=0.0, scale=std, size=len(sols)) + + perturbed = Population.new(**out) + TotalConstraintViolation().do(perturbed, inplace=True) + return perturbed diff --git a/src/pysamoo/core/selection.py b/src/pysamoo/core/selection.py new file mode 100644 index 0000000..d6b9c33 --- /dev/null +++ b/src/pysamoo/core/selection.py @@ -0,0 +1,40 @@ +"""Pluggable model-selection strategies. + +A model-selection strategy is just a **Target factory**: a callable +``(label, models) -> Target`` that decides how a pool of candidate surrogates is +cross-validated and reduced to the one that is used. + +Built-in strategies: + +* ``"full"`` — :class:`~pysamoo.core.target.Target`: cross-validate the whole + pool every iteration (most accurate, slowest). + +Add a new strategy by registering another :class:`~pysamoo.core.target.Target` +subclass in :data:`STRATEGIES`, or pass any factory directly to an algorithm's +``selection`` argument. This is the single extension point — algorithms do +not hard-code which strategy they use. +""" + +from pysamoo.core.target import Target + +STRATEGIES = { + "full": Target, +} + + +def resolve(strategy): + """Resolve a ``selection`` value to a Target factory. + + Args: + strategy: Either a registered name (a key of :data:`STRATEGIES`) or a + callable ``(label, models) -> Target`` (e.g. a ``Target`` subclass or a + ``lambda`` that pre-configures one). + + Returns: + A callable ``(label, models) -> Target``. + """ + if callable(strategy): + return strategy + if strategy in STRATEGIES: + return STRATEGIES[strategy] + raise ValueError(f"unknown selection {strategy!r}; choose from {sorted(STRATEGIES)} or pass a Target factory") diff --git a/src/pysamoo/core/surrogate.py b/src/pysamoo/core/surrogate.py new file mode 100644 index 0000000..5e8fd24 --- /dev/null +++ b/src/pysamoo/core/surrogate.py @@ -0,0 +1,59 @@ +"""Surrogate-model wrapper and surrogate-backed problem definition.""" + +import numpy as np +from pymoo.core.meta import Meta + + +class Surrogate: + def __init__(self, problem, targets=None, **kwargs): + """Build and update surrogates for the components of a Population. + + Objectives and inequality/equality constraints may each need their own model, and different model + combinations are possible; this object wraps that bookkeeping behind fit/validate/predict. + + Args: + problem: The optimization problem, used only for its metadata (it is never evaluated here). + targets: Target objects describing how each population component is modeled (model type and + hyper-parameters). The modular definition allows a different surrogate per target. + """ + + super().__init__(**kwargs) + self._problem = problem + self.targets = targets if targets is not None else [] + + def validate(self, trn=None, tst=None, random_state=None, **kwargs): + for target in self.targets: + target.validate(trn=trn, tst=tst, random_state=random_state, **kwargs) + + def fit(self, sols): + for target in self.targets: + target.fit(sols) + + def performance(self, indicator, **kwargs): + ret = {} + for target in self.targets: + ret[target.label] = target.performance(indicator, **kwargs) + return ret + + def problem(self): + return ProblemFromTargets(self._problem, self.targets) + + +class ProblemFromTargets(Meta): + def __init__(self, problem, targets, **kwargs): + super().__init__(problem, **kwargs) + self.targets = targets + + def _evaluate(self, X, out, *args, **kwargs): + n = len(X) + + out["F"] = np.full((n, self.n_obj), np.nan, dtype=float) + out["G"] = np.full((n, self.n_ieq_constr), np.nan, dtype=float) + out["H"] = np.full((n, self.n_eq_constr), np.nan, dtype=float) + + for target in self.targets: + target.predict(X, out) + + for v in ["F", "G", "H"]: + if np.any(np.isnan(out[v])): + raise RuntimeError(f"Surrogate prediction produced NaN values in '{v}'; the run was terminated.") diff --git a/pysamoo/core/target.py b/src/pysamoo/core/target.py similarity index 61% rename from pysamoo/core/target.py rename to src/pysamoo/core/target.py index 9f7d191..6d9721a 100644 --- a/pysamoo/core/target.py +++ b/src/pysamoo/core/target.py @@ -1,10 +1,10 @@ +"""Target specification mapping problem outputs to surrogate models.""" + from copy import deepcopy import numpy as np from ezmodel.core.benchmark import Benchmark from ezmodel.core.partitioning import merge_and_partition -from ezmodel.util.partitioning.crossvalidation import CrossvalidationPartitioning -from pymoo.core.population import Population from pymoo.util.misc import from_dict from pymoo.util.sliding_window import SlidingWindow @@ -12,38 +12,17 @@ class Target: - - def __init__(self, - label, - models, - n_folds=5, - n_max_performances=5, - n_max_benchmarks=0, - indicators=INDICATORS): - """ - - Parameters - ---------- - label : tuple - This describes the type and what role it plays in the problem later on. For instance, if this target - models the first constraint ('G', 0) would be the corresponding type. - - models : list - A list of all models which are kept track of for this target. - - n_folds : int - The number of folds if cross-validation is applied (happens only the first time) - - n_max_performances : int - The number of maximum last performances to make a decision - - n_max_benchmarks : int - The maximum number of benchmarks (only to review later) - - indicators : list - A list of tuples (name, sign, func) defining what indicators should be calculated after - each of the benchmarks. - + def __init__(self, label, models, n_folds=5, n_max_performances=5, indicators=INDICATORS): + """Track a pool of candidate models for one problem output and select the best. + + Args: + label: A ``(type, index)`` tuple describing what this target models and its role in the + problem — e.g. ``('G', 0)`` for the first inequality constraint. + models: Mapping of model name to model object kept as candidates for this target. + n_folds: Number of folds used for cross-validation (only on the first validation). + n_max_performances: How many recent performances to retain per model for the decision. + indicators: Mapping of indicator name to ``{sign, func}`` describing the accuracy metrics + computed after each benchmark. """ self.label = label @@ -53,30 +32,38 @@ def __init__(self, # the indicators to be calculated for this target self.indicators = indicators - # this is the storage for the most recent benchmarks - full experiment - self.benchmarks = SlidingWindow(size=n_max_benchmarks) - # this keeps track of the past performances - each model gets one self.performances = {} for key in models.keys(): self.performances[key] = SlidingWindow(size=n_max_performances) - # the current name of the model which is used - self.model = None - # the actual model fitting the data points provided self.obj = None # the best model set by the last validation self.best = None - def validate(self, trn, tst=None, find_best=True, **kwargs): + def _cv_folds(self, n, random_state=None): + """Build k-fold (train, test) index partitions. + + Points are shuffled before strided assignment so a fold is never biased + toward the order in which the optimizer produced points (early-exploration + vs late-exploitation). The shuffle uses the run's ``random_state`` + Generator, so folds are randomized yet fully reproducible. Without a + Generator it falls back to deterministic strided folds. + """ + k = min(self.n_folds, n) + order = random_state.permutation(n) if random_state is not None else np.arange(n) + pos = np.arange(n) + return [(list(order[pos % k != f]), list(order[pos % k == f])) for f in range(k)] + + def validate(self, trn, tst=None, find_best=True, random_state=None, **kwargs): # get the values to be predicted X, y = trn.get("X"), self._get_y(trn) if tst is None: - X, y, partitions = X, y, CrossvalidationPartitioning(self.n_folds).do(len(trn)) + partitions = self._cv_folds(len(trn), random_state) else: _X, _y = tst.get("X"), self._get_y(tst) X, y, partitions = merge_and_partition((X, y), (_X, _y)) @@ -84,10 +71,8 @@ def validate(self, trn, tst=None, find_best=True, **kwargs): # do the benchmark for the specific target given the partitions obj = Benchmark(self.models, raise_exception=False).do(X, y, partitions=partitions) benchmark = obj.results(only_successful=False, as_list=False, include_metadata=True) - self.benchmarks.append(benchmark) for model in self.models.keys(): - results = benchmark["results"][model] # for each of the run, execute all the performance indicators @@ -99,7 +84,7 @@ def validate(self, trn, tst=None, find_best=True, **kwargs): if find_best: self.best = self.find_best(**kwargs) - def find_best(self, indicator=["kendall_tau", "mae"], exclude=[]): + def find_best(self, indicator=("kendall_tau", "mae"), exclude=()): models = [model for model in self.models.keys() if model not in exclude] perf = self.performances @@ -110,8 +95,7 @@ def find_best(self, indicator=["kendall_tau", "mae"], exclude=[]): assert len(models) > 0, "Fitting each of the models has failed at least once in the benchmark." for entry in indicator: - - # get the performances from the the n_max_performance iterations + # get the performances from the n_max_performance iterations v = np.array([self.performance(entry, model=model) for model in models]) # multiply by the sign to consider minimization and maximization @@ -123,8 +107,10 @@ def find_best(self, indicator=["kendall_tau", "mae"], exclude=[]): if len(models) == 1: break - # finally find the best model using the indicator - return np.random.choice(models) + # Deterministic tie-break: keep the first survivor in the (stable) pool + # order. kendall_tau returns an integer disorder count, so ties are common; + # a random pick here was a key source of run-to-run irreproducibility. + return models[0] def fit(self, sols): assert self.best is not None, "You need to do one initial validation to find the best model for this target." @@ -136,7 +122,6 @@ def fit(self, sols): X, y = sols.get("X"), self._get_y(sols) obj.fit(X, y) - self.model = self.best self.obj = obj def performance(self, indicator, model=None, func=np.mean): @@ -154,7 +139,8 @@ def performance(self, indicator, model=None, func=np.mean): def predict(self, X, out): assert self.obj is not None, "The target has not been fitted yet." - v = self.obj.predict(X) + # ezmodel predict returns a Prediction wrapper; take its mean (.y) + v = self.obj.predict(X).y key, index = self.label out.get(key)[:, [index]] = v @@ -177,20 +163,16 @@ def _indicators(self, benchmark, run): return ret def _get_y(self, sols): - """ - Parameters - ---------- - sols : Population - A set of solutions. + """Extract the values this target models from a set of solutions. - Returns - ------- - Y : np.array - The Y values to be modeled by the surrogate + Args: + sols: A population of solutions. + Returns: + The 1-D array of values (for this target's output column) to be modeled by the surrogate. """ key, index = self.label return sols.get(key)[:, index] def __repr__(self) -> str: - return f"({self.label[0]}, {self.label[1]}) : {super(Target, self).__repr__()}" + return f"({self.label[0]}, {self.label[1]}) : {super().__repr__()}" diff --git a/pysamoo/core/tcv.py b/src/pysamoo/core/tcv.py similarity index 55% rename from pysamoo/core/tcv.py rename to src/pysamoo/core/tcv.py index 5e4498a..b383ddb 100644 --- a/pysamoo/core/tcv.py +++ b/src/pysamoo/core/tcv.py @@ -1,51 +1,37 @@ +"""Total constraint-violation aggregation utilities.""" + from collections.abc import Callable -import autograd.numpy as anp import numpy as np - from pymoo.core.individual import Individual from pymoo.core.population import Population from pymoo.util.misc import at_least_2d_array class TotalConstraintViolation: - - def __init__(self, - ieq_eps: float = 0.0, - ieq_pow: float = None, - ieq_scale: np.ndarray = None, - eq_eps: float = 1e-4, - eq_pow: float = None, - eq_scale: np.ndarray = None, - aggr_func: Callable = np.mean, - feas_eps: float = 0.0): - - """ - - Parameters - ---------- - ieq_pow : float - To what power the each inequality constraint violation should be taken - - ieq_eps : float - The allowed violation of an inequality constraint (usually 0, but might be relaxed during a run) - - ieq_scale : np.array - The scaling for the inequality constraints to consider. The cvs will be divided by this scaling. - (useful if constraints have entirely different scales which might cause a biased aggregation) - - eq_pow : float - To what power the each equality constraint violation should be taken - - eq_eps : float - The permitted violation of an equality constraint - small eps value defined in config - - eq_scale : np.array - Same as `ieq_scale` but for equality constraints. - - feas_eps : float - The eps amount for a solution to count as feasible or infeasible. - + def __init__( + self, + ieq_eps: float = 0.0, + ieq_pow: float | None = None, + ieq_scale: np.ndarray | None = None, + eq_eps: float = 1e-4, + eq_pow: float | None = None, + eq_scale: np.ndarray | None = None, + aggr_func: Callable = np.mean, + feas_eps: float = 0.0, + ): + """Aggregate inequality/equality constraints into a single total-constraint-violation value. + + Args: + ieq_eps: Allowed violation of an inequality constraint (usually 0, may be relaxed during a run). + ieq_pow: Power each inequality-constraint violation is raised to. + ieq_scale: Per-constraint scaling; the violations are divided by it (useful for differently + scaled constraints that would otherwise bias the aggregation). + eq_eps: Permitted violation of an equality constraint (a small epsilon). + eq_pow: Power each equality-constraint violation is raised to. + eq_scale: Same as ``ieq_scale`` but for equality constraints. + aggr_func: Aggregation applied across constraints to obtain the total violation. + feas_eps: The violation threshold below which a solution counts as feasible. """ super().__init__() @@ -62,21 +48,18 @@ def __init__(self, self.feas_eps = feas_eps - def calc(self, - G: np.ndarray = None, - H: np.ndarray = None, - return_feas=False): + def calc(self, G: np.ndarray = None, H: np.ndarray = None, return_feas=False): # convert all constraints to one big array C = [] if G is not None: - G = at_least_2d_array(G, extend_as='r') + G = at_least_2d_array(G, extend_as="r") cv_ieq = g_to_cv(G, self.ieq_eps, beta=self.ieq_beta, scale=self.ieq_scale) C.append(cv_ieq) if H is not None: - H = at_least_2d_array(H, extend_as='r') + H = at_least_2d_array(H, extend_as="r") cv_eq = g_to_cv(np.abs(H), self.eq_eps, beta=self.eq_beta, scale=self.eq_scale) # cv_eq = g_to_cv(H ** 2, self.eq_eps ** 2, beta=self.eq_pow, scale=self.eq_scale) C.append(cv_eq) @@ -85,10 +68,10 @@ def calc(self, if len(C) == 0: return None - C = anp.column_stack(C) + stacked = np.column_stack(C) # calculate the total constraint violation - tcv = self.aggr_func(C, axis=1) + tcv = self.aggr_func(stacked, axis=1) if return_feas: return tcv, tcv <= self.feas_eps @@ -99,7 +82,7 @@ def do(self, pop, inplace=True): # this way the total constraint violation calculation also works for an individual if isinstance(pop, Individual): - pop = Population().create(pop) + pop = Population.create(pop) # do the actual calculations to get the total constraint violations G, H = pop.get("G", "H") @@ -119,11 +102,10 @@ def do(self, pop, inplace=True): def g_to_cv(g, eps, beta=None, scale=None): # subtract eps to allow some violation and then zero out all values less than zero - g = anp.maximum(0.0, g - eps) + g = np.maximum(0.0, g - eps) # apply scaling if necessary if scale is not None: - # allow a scalar value as input if not isinstance(scale, np.ndarray): scale = np.full(g.shape[1], scale) @@ -134,15 +116,6 @@ def g_to_cv(g, eps, beta=None, scale=None): # if a pow factor has been provided if beta is not None: - g = g ** beta + g = g**beta return g - - -def estm_scale(v, eps=0.0, func=np.mean): - v = v[v > eps] - - if len(v) == 0: - return 1.0 - else: - return func(v) diff --git a/src/pysamoo/experimental/RESEARCH_PLAN.md b/src/pysamoo/experimental/RESEARCH_PLAN.md new file mode 100644 index 0000000..3f6d8df --- /dev/null +++ b/src/pysamoo/experimental/RESEARCH_PLAN.md @@ -0,0 +1,87 @@ +# BO Method Search — Research Plan + +Goal: a **single, fast** `BayesianOptimization` default that performs well across the whole +difficulty spectrum, measured by the ECDF benchmark (`experimental/benchmark.py`). "Fast" is a +first-class constraint — the benchmark is time-boxed (~20s) and wall-clock is part of the score. + +## 1. The oracle + +`experimental/benchmark.py` (do **not** edit when iterating) runs the suite below at 2D and 10D, +optimum `F*=0`, budget `30 + 5d` evals, **3 seeds**, and reports **ECDF** = fraction of +`(function × target × seed)` solved, where targets are **scale-normalized** per function +(`R_f·10^(-k)`, `k∈[0,8]`) so every function gives a smooth gradient. (2 seeds was too noisy to +reward real gains — hardened to 3.) + +**Original baseline (AutoModel select): tuned 0.468 / held-out 0.438, ~31s.** +**Shipped default (single Kriging[exp], upstream pg4/pool256): tuned 0.473 / held-out 0.458, ~14s** +— a modest but *generalizing* win (2x faster, stable). Bigger tuned-only gains (pg6, pool2048) were +**overfitting** — they raised tuned ECDF but lowered held-out. Always check both (`evaluate_heldout`). + +## 2. Problem taxonomy — what each function tests + +| function | modality | conditioning | separable | tests | final 10D | +|---|---|---|---|---|---| +| Sphere | uni | well | yes | baseline (local refinement) | solved ✓ | +| Rosenbrock | uni | ill | **no** | **local** model / curved valley | 0.16 (open) | +| Rastrigin | **multi** | well | yes | **global** exploration (funnel) | 0.10 (open) | +| RotatedEllipsoid | uni | **ill (1e6)** | **no** | **metric / rotation** learning | 0.49 (was 0.22) | + +The three 10D failures each demand a *different* fix — local model (Rosenbrock), global +exploration (Rastrigin), learned metric (Ellipsoid). A good global+local method lifts all three. + +## 3. Method ideas (from the literature), ranked by expected payoff + +Global+local is the dominant paradigm (Ong et al. 2003). Combination patterns: **switch** +(current Hybrid), **competition** (LAGO — both propose each step, best wins), **localize the global +model** (TuRBO — local GP in adaptive trust regions), **memetic filter-then-refine**. + +Ideas to try, each a self-contained change in `experimental/`: + +1. **Competition Hybrid (LAGO-style)** — global EI *and* local each propose every iteration; take + whichever actually improves. Removes the brittle patience switch. *Helps: all.* +2. **PCA-rotated local (LABCAT-style)** — weighted-PCA rotate the local points, fit ARD-GP in the + rotated frame (= a Mahalanobis kernel for free), EI in a rotated trust region. *Helps: + RotatedEllipsoid, Rosenbrock.* +3. **Better local model** — replace the single quadratic with a *moving* trust region that refits + each step (NEWUOA-style), or an RBF/local-GP local model that follows a curved valley. *Helps: + Rosenbrock.* +4. **Multiple trust regions / restarts (TuRBO-style)** — several local regions to escape Rastrigin + basins; restart the global phase from diverse seeds. *Helps: Rastrigin.* +5. **Acquisition-space whitening** — search EI in coordinates whitened by the local Hessian / sample + covariance, so EI explores along the valley. *Helps: Rosenbrock, Ellipsoid.* +6. **CMA-style covariance adaptation** for the local sampling distribution. *Helps: Ellipsoid, + Rosenbrock.* + +## 4. Iteration protocol (the loop) + +Each iteration: pick one idea → implement in `experimental/` (bo/infill/optimizer/acquisition only; +**never** benchmark.py/problems.py) → `pyclawd check` must pass → run benchmark → keep only if +`score` (ECDF, speed tie-break) beats the best AND time stays ~≤25s → append to `findings.md`. +Time-boxed; keep the best version. Agents compare, humans bless commits. + +## 5. Success criteria + +- Strictly beat baseline ECDF 0.359 while staying fast (~≤25s benchmark). +- Stretch: solve ≥1 of the three 10D-failing functions to a non-trivial target without regressing + Sphere or the 2D cases. + +## References + +- Ong, Nair, Keane (2003) — Combining global and local surrogate models. +- Eriksson et al. (2019) — TuRBO: Scalable Global Optimization via Local BO. +- LABCAT (2023, arXiv:2311.11328) — PCA-aligned trust regions. +- CMA-BO (2024, arXiv:2402.03104) — covariance matrix adaptation for BO. +- LAGO (2026, arXiv:2603.02970) — local-global competition framework. +- ALEBO (NeurIPS 2020) — Mahalanobis kernel for linear embeddings. +- Ament et al. (2023) — LogEI (numerically stable acquisition). + +## 6. Open ideas (untried / in progress) + +- **Sequential CMA-ES local strategy** (`LocalCMAES`): learns the metric (covariance ~ inverse + Hessian) — the proven method for curved-valley (Rosenbrock) and ill-conditioned (Ellipsoid) + problems. One sample per infill, rank-mu update each generation. *In progress.* +- **Periodic AutoModel re-selection** (user idea, 2026-06-30): we dropped per-infill `AutoModel` + for fixed `Kriging[exp]` (speed). But re-running *full* selection occasionally (every N evals, + not every infill) could recover AutoModel's adaptivity cheaply — and crucially lets the model + *type* switch as more data reveals structure (a kernel that wins at 20 pts may lose at 80). Test + `AutoModel` refit-from-scratch every ~15 evals vs fixed Kriging[exp]. Open question: worth the cost? diff --git a/src/pysamoo/experimental/__init__.py b/src/pysamoo/experimental/__init__.py new file mode 100644 index 0000000..fa4696f --- /dev/null +++ b/src/pysamoo/experimental/__init__.py @@ -0,0 +1 @@ +"""Research scaffolding for the BO method search (acquisitions, infills, optimizers, oracle benchmark).""" diff --git a/src/pysamoo/experimental/acquisition.py b/src/pysamoo/experimental/acquisition.py new file mode 100644 index 0000000..2448f78 --- /dev/null +++ b/src/pysamoo/experimental/acquisition.py @@ -0,0 +1,247 @@ +"""Acquisition functions (EI, LogEI, POI, UCB) and their optimization-problem wrappers.""" + +import numpy as np +from pymoo.core.meta import Meta +from pysurrogate.core.optimizer import Evaluation, Problem +from scipy.special import erfcx, log_ndtr, ndtr +from scipy.stats import norm + +_SQRT_2 = np.sqrt(2.0) +_SQRT_2PI = np.sqrt(2.0 * np.pi) +_SQRT_HALF_PI = np.sqrt(np.pi / 2.0) +_LOG_SQRT_2PI = 0.5 * np.log(2.0 * np.pi) + + +def _log_h(z): + """Numerically stable ``log h(z)`` where ``h(z) = z*Phi(z) + phi(z)`` (so ``EI = sigma*h(z)``). + + ``h`` is the standardized expected improvement; it is positive but underflows to ``0`` in + float64 once ``z`` is very negative (the incumbent is already good), which is what makes plain + EI -- and its gradient -- vanish. This computes ``log h`` directly so it stays finite for any + ``z``: + + - ``z > -1``: the direct ``log(z*Phi + phi)`` is accurate and free of underflow. + - ``z <= -1``: the Mills-ratio form ``h = phi(z) * (1 + z * Phi(z)/phi(z))`` with + ``Phi/phi = sqrt(pi/2)*erfcx(-z/sqrt2)`` -- ``erfcx`` is stable for large arguments, so the + bracket is evaluated without the cancellation that kills the naive expression. + + Args: + z: Standardized improvement ``(f_min - mu) / sigma``, any shape. + + Returns: + ``log h(z)``, same shape as ``z``. + """ + z = np.asarray(z, dtype=float) + out = np.empty_like(z) + + upper = z > -1.0 + zu = z[upper] + out[upper] = np.log(zu * ndtr(zu) + np.exp(-0.5 * zu**2) / _SQRT_2PI) + + zl = z[~upper] + log_phi = -0.5 * zl**2 - _LOG_SQRT_2PI + bracket = 1.0 + zl * _SQRT_HALF_PI * erfcx(-zl / _SQRT_2) + out[~upper] = log_phi + np.log(np.clip(bracket, 1e-300, None)) + return out + + +# ========================================================================================================= +# Acquisition Functions +# ========================================================================================================= + + +class AcquisitionFunction: + def calc(self, mu, sigma, **kwargs): + pass + + +class EI(AcquisitionFunction): + def calc(self, mu, sigma, f_min=None, **kwargs): + if f_min is None: + raise Exception("Estimation of minimum function value needs to be provided!") + + f = -sigma + + # through precision error sigma can be negative - this should actually never be the case + pos_sigma = sigma > 0 + mu, sigma = mu[pos_sigma], sigma[pos_sigma] + + # minimization version of EI + impr = f_min - mu + + # calculate expected improvement + z = impr / sigma + + ei = impr * norm.cdf(z) + sigma * norm.pdf(z) + + # because we are minimizing take the negative expected improvement + f[pos_sigma] = -ei + + return f + + +class LogEI(AcquisitionFunction): + """Logarithm of Expected Improvement -- the numerically stable acquisition. + + Plain EI underflows to ``0`` (and so does its gradient) once the incumbent is good, because + ``z = (f_min - mu)/sigma`` is very negative everywhere; a gradient optimizer then has no + signal and the search stalls. ``LogEI`` returns ``log(EI) = log(sigma) + log h(z)`` computed + in a stable closed form, which stays finite and keeps a usable gradient even when EI itself is + ~1e-300 (Ament et al., NeurIPS 2023). The argmax is identical to EI's, since ``log`` is + monotonic. ``calc`` returns ``-log(EI)`` to match the minimization convention. + """ + + def calc(self, mu, sigma, f_min=None, **kwargs): + if f_min is None: + raise Exception("Estimation of minimum function value needs to be provided!") + sigma = np.maximum(sigma, 1e-300) + z = (f_min - mu) / sigma + return -(np.log(sigma) + _log_h(z)) + + +class POI(AcquisitionFunction): + def calc(self, mu, sigma, f_min=None, **kwargs): + if f_min is None: + raise Exception("Estimation of minimum function value needs to be provided!") + + pos_sigma = sigma > 0 + mu, sigma = mu[pos_sigma], sigma[pos_sigma] + f = sigma + + # minimization version of PI + impr = f_min - mu + + # calculate pi + z = impr / sigma + pi = norm.pdf(z) + + f[pos_sigma] = -pi + + return f + + +class UCB(AcquisitionFunction): + def __init__(self, beta=3.0) -> None: + super().__init__() + self.beta = beta + + def calc(self, mu, sigma, **kwargs): + ucb = mu - self.beta * sigma + return ucb + + +# ========================================================================================================= +# Acquisition Problem +# ========================================================================================================= + + +class AcquisitionProblem(Meta): + def __init__(self, problem, model, acquisition_func, **kwargs): + super().__init__(problem) + self.model = model + self.acquisition_func = acquisition_func + self.kwargs = kwargs + + def _evaluate(self, x, out, *args, **kwargs): + if self.model is None: + raise Exception("Please set the model for the problem to be defined.") + + # calculate the metric using the implementation (Dace returns a Prediction) + pred = self.model.predict(x, var=True) + mu, sigma = pred.y[:, 0], pred.sigma[:, 0] + + # calculate the value of the acquisition function + acq = self.acquisition_func.calc(mu, sigma, x=x, **self.kwargs) + + out["F"], out["acq"] = acq, acq + + +# ========================================================================================================= +# Expected Improvement as a pysurrogate optimization Problem +# ========================================================================================================= + + +class EIProblem(Problem): + """(Log) Expected-Improvement maximization as a pysurrogate minimization ``Problem``. + + Lets a generic ``pysurrogate.optimizer`` strategy (``Adam``, ``LBFGS``, ``Restart``) maximize + EI over the box -- the same layer pysurrogate uses to fit theta -- instead of a bespoke + acquisition optimizer. EI is maximized by *minimizing* ``-EI`` (or ``-log EI``); the analytic + gradient comes from the surrogate's mean/variance gradients (``predict(mse=True, grad=True)``), + so the search is gradient-driven and needs no seed pool. + + With ``log=True`` (the default) it optimizes ``log EI`` instead of ``EI``. They share an + argmax (``log`` is monotonic), but ``log EI`` does not underflow to ``0`` once the incumbent is + good, so its gradient stays finite and the search keeps converging instead of stalling. + + The search runs in the unit cube ``[0, 1]^d`` (``__call__`` maps each candidate back to the + real box) so an optimizer's step size is a fraction of each dimension's width, independent of + the problem's actual scale. + + Args: + model: The surrogate exposing ``predict(mse=True, grad=True)`` (a Dace model). + f_min: The incumbent objective EI improves over (the best observed value). + xl: Lower bounds of the real problem box, shape ``(d,)``. + xu: Upper bounds of the real problem box, shape ``(d,)``. + log: Optimize ``log EI`` (stable, default) rather than ``EI``. + """ + + def __init__(self, model, f_min, xl, xu, log=True): + self.model = model + self.f_min = f_min + self.log = log + self._xl = np.asarray(xl, dtype=float) + self._span = np.asarray(xu, dtype=float) - self._xl + + @property + def bounds(self): + """The unit-cube search box ``(0, 1)^d`` the optimizer works in.""" + p = len(self._xl) + return np.zeros(p), np.ones(p) + + def to_x(self, U): + """Map unit-cube candidates ``U`` back to the real problem box.""" + return self._xl + np.atleast_2d(U) * self._span + + def screen(self, U): + """Cheap value-only ``-(log)EI`` for ranking a seed pool (no gradient computed).""" + X = self.to_x(U) + pred = self.model.predict(X, var=True) + mu = pred.y[:, 0] + sigma = np.maximum(pred.sigma[:, 0], 1e-300 if self.log else 1e-12) + z = (self.f_min - mu) / sigma + if self.log: + return -(np.log(sigma) + _log_h(z)) + return -((self.f_min - mu) * norm.cdf(z) + sigma * norm.pdf(z)) + + def __call__(self, U): + """Return ``-(log)EI`` and its gradient (w.r.t. the unit cube) at the candidates ``U``.""" + X = self.to_x(U) + pred = self.model.predict(X, var=True, grad=True) + mu = pred.y[:, 0] + sigma = np.maximum(pred.sigma[:, 0], 1e-300 if self.log else 1e-12) + + z = (self.f_min - mu) / sigma + # chain-rule pieces shared by both forms: dmu = pred.grad, dsigma = grad(var)/(2 sigma) + dmu = pred.grad + dsigma = pred.var_grad / (2.0 * sigma[:, None]) + + if self.log: + # log EI = log(sigma) + log h(z); h'(z) = Phi(z), so d logEI/dx = + # (1/sigma)[ -r dmu + dsigma (1 - z r) ] with r = Phi(z)/h(z) (computed in log-space, + # so it stays finite where EI itself underflows). + log_h = _log_h(z) + r = np.exp(log_ndtr(z) - log_h) + f = -(np.log(sigma) + log_h) + d_log_ei = (-r[:, None] * dmu + dsigma * (1.0 - z * r)[:, None]) / sigma[:, None] + grad_x = -d_log_ei + else: + Phi, phi = norm.cdf(z), norm.pdf(z) + ei = (self.f_min - mu) * Phi + sigma * phi + f = -ei + # gradient of -EI w.r.t. x: -(-Phi*dmu + phi*dsigma) + grad_x = Phi[:, None] * dmu - phi[:, None] * dsigma + + grad = grad_x * self._span[None, :] + feasible = np.isfinite(f) + return Evaluation(f=np.where(feasible, f, np.inf), feasible=feasible, grad=grad) diff --git a/src/pysamoo/experimental/benchmark.py b/src/pysamoo/experimental/benchmark.py new file mode 100644 index 0000000..027115b --- /dev/null +++ b/src/pysamoo/experimental/benchmark.py @@ -0,0 +1,139 @@ +"""Fast BBOB-style ECDF benchmark for the BO method search (ORACLE -- do not edit when iterating). + +The feedback function the method-search loop optimizes against. It runs the configured +``BayesianOptimization`` on the fixed suite (Sphere / Rosenbrock / Rastrigin / RotatedEllipsoid at +2D and 10D, optimum F*=0) and reports a single **ECDF score** in [0, 1]. + +Design goal: be a *good gradient* for method development, not just a pass/fail. The targets are +**scale-normalized per function** -- decades below each function's own typical magnitude -- so +*partial progress always registers* (improving 10D Rosenbrock 220->50 moves the score) and all +functions are comparable despite spanning ~6 orders of magnitude. ECDF = fraction of +``(function x target x seed)`` triples reached within the evaluation budget. Wall-clock is part of +the score because the method must stay fast. Tuned to run in ~20s. +""" + +import time +import warnings + +import numpy as np +from pymoo.optimize import minimize + +from pysamoo.experimental.problems import benchmark_problems, heldout_problems + +warnings.filterwarnings("ignore") + +# targets are R_f * 10^(-k): decades below each function's reference scale R_f. 17 levels over 8 +# decades = half-decade granularity, so the ECDF behaves like a smooth normalized log-regret. +TARGET_DECADES = np.linspace(0.0, 8.0, 17) + +# 3 seeds, not 2: with only 2 seeds the ECDF was too noisy to reliably reward real improvements +# (a genuine +3% gain on the hard 10D functions landed on seeds 3-5 and was invisible on seeds 1-2). +# 3 seeds is the speed/reliability compromise that keeps the run near the ~20s iteration budget. +SEEDS = (1, 2, 3) + + +def budget_for(d): + """Return the function-evaluation budget for a ``d``-dimensional problem.""" + return 30 + 5 * d + + +def reference_scale(problem, n=2000, seed=0): + """Reference magnitude R_f for a function: the median objective over random points in its box. + + Targets are taken as decades below this, so every function -- whether its values are ~1 or ~1e6 + -- contributes a meaningful, comparable 0..1 progress signal. + + Args: + problem: The pymoo problem. + n: Number of random points to estimate the scale from. + seed: RNG seed (fixed, so the scale is deterministic). + + Returns: + Median objective value over the random sample (a positive float). + """ + rng = np.random.default_rng(seed) + X = problem.xl + rng.random((n, problem.n_var)) * (problem.xu - problem.xl) + F = problem.evaluate(X)[:, 0] + return float(np.median(F)) + + +def make_default_algorithm(): + """Build a fresh BayesianOptimization with the current default configuration (the thing under test).""" + from pysamoo.experimental.bo import BayesianOptimization + + return BayesianOptimization() + + +def run_one(problem, seed, make_algorithm): + """Run one optimization; return the best objective reached (gap to optimum, since F*=0).""" + res = minimize(problem, make_algorithm(), ("n_evals", budget_for(problem.n_var)), seed=seed, verbose=False) + return float(res.F[0]) + + +def evaluate_on(problems, make_algorithm=make_default_algorithm, seeds=SEEDS, verbose=True): + """Run a given problem suite and return its ECDF score, wall-clock, and per-problem breakdown. + + Shared core of :func:`evaluate` (the tuned suite) and :func:`evaluate_heldout` (the held-out + generalization suite). A method should improve *both*; a gain on the tuned suite that does not + carry to the held-out suite is overfitting and must not be kept. + + Args: + problems: List of ``(name, problem)`` to run. + make_algorithm: Zero-arg factory returning a fresh algorithm to benchmark. + seeds: Seeds run per problem. + verbose: Print a per-problem table and the headline score. + + Returns: + Dict with ``ecdf``, ``time``, ``per_problem``, and ``score`` (ecdf with a speed tie-break). + """ + t0 = time.time() + per_problem, solved_flags = {}, [] + for name, problem in problems: + ref = reference_scale(problem) + targets = ref * 10.0 ** (-TARGET_DECADES) + best = [run_one(problem, s, make_algorithm) for s in seeds] + solved = [[b <= t for t in targets] for b in best] + solved_flags.extend(np.array(solved).ravel().tolist()) + frac = float(np.mean(solved)) + per_problem[name] = {"best": best, "solved_frac": frac, "ref": ref} + if verbose: + print(f" {name:20} bestF={np.median(best):.2e} ref={ref:.1e} solved={frac:5.2f}", flush=True) + elapsed = time.time() - t0 + ecdf = float(np.mean(solved_flags)) + score = ecdf - 1e-4 * elapsed + if verbose: + print(f"\n ECDF = {ecdf:.4f} time = {elapsed:.1f}s score = {score:.4f}", flush=True) + return {"ecdf": ecdf, "time": elapsed, "per_problem": per_problem, "score": score} + + +def evaluate_heldout(make_algorithm=make_default_algorithm, seeds=SEEDS, verbose=True): + """Run the held-out suite (Ackley/Griewank/Zakharov) -- the overfitting guard. See :func:`evaluate_on`.""" + return evaluate_on(heldout_problems(), make_algorithm=make_algorithm, seeds=seeds, verbose=verbose) + + +def evaluate(make_algorithm=make_default_algorithm, seeds=SEEDS, verbose=True): + """Run the full suite and return the ECDF score, wall-clock, and per-problem breakdown. + + Args: + make_algorithm: Zero-arg factory returning a fresh algorithm to benchmark. + seeds: Seeds run per problem (more = less noisy ECDF, more time). + verbose: Print a per-problem table and the headline score. + + Returns: + Dict with ``ecdf`` (scalar in [0,1]), ``time`` (seconds), ``per_problem`` (name -> best F + and solved-fraction), and ``score`` (ecdf with a tiny speed tie-break). + """ + return evaluate_on(benchmark_problems(), make_algorithm=make_algorithm, seeds=seeds, verbose=verbose) + + +def main(): + """Run the tuned and held-out suites on the current default algorithm and print both reports.""" + print(f"BO ECDF benchmark — {len(benchmark_problems())} tuned problems, scale-normalized targets\n") + tuned = evaluate() + print(f"\nHeld-out generalization suite ({len(heldout_problems())} problems — never tuned against):\n") + held = evaluate_heldout() + print(f"\n SUMMARY: tuned ECDF = {tuned['ecdf']:.4f} held-out ECDF = {held['ecdf']:.4f}") + + +if __name__ == "__main__": + main() diff --git a/src/pysamoo/experimental/bo.py b/src/pysamoo/experimental/bo.py new file mode 100644 index 0000000..c0009d4 --- /dev/null +++ b/src/pysamoo/experimental/bo.py @@ -0,0 +1,187 @@ +"""Bayesian optimization over a pluggable pysurrogate surrogate (model selection by default).""" + +import matplotlib.pyplot as plt +import numpy as np +from pymoo.algorithms.soo.nonconvex.ga import FitnessSurvival +from pymoo.core.callback import Callback +from pymoo.core.population import Population +from pymoo.termination.default import DefaultSingleObjectiveTermination +from pymoo.util.display.column import Column +from pymoo.util.display.single import SingleObjectiveOutput +from pysurrogate.dace import Exponential +from pysurrogate.models import Kriging + +from pysamoo.core.algorithm import SurrogateAssistedAlgorithm +from pysamoo.experimental.acquisition import AcquisitionProblem, LogEI +from pysamoo.experimental.infill import GlobalEI, Hybrid +from pysamoo.experimental.optimizer import VectorizedGradientDescent + +# --------------------------------------------------------------------------------------------------------- +# Display +# --------------------------------------------------------------------------------------------------------- + + +class EGOOutput(SingleObjectiveOutput): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.output = SingleObjectiveOutput() + + self.f_new = Column(name="f_new") + self.acq = Column(name="acq") + + def initialize(self, algorithm): + self.output.initialize(algorithm) + self.columns = self.output.columns + [self.f_new, self.acq] + + def update(self, algorithm): + bo = algorithm + self.output.update(bo) + + if algorithm.acq is not None: + self.f_new.set(bo.infills.get("F").min()) + self.acq.set(bo.infills.get("acq").min()) + + +# --------------------------------------------------------------------------------------------------------- +# Implementation +# --------------------------------------------------------------------------------------------------------- + + +def default_surrogate(): + """The default BO surrogate: a single Exponential-kernel Kriging model. + + The method-search benchmark (``experimental/benchmark.py``) showed a *fixed* ``Kriging[exp]`` + beats cross-validated ``AutoModel`` over the Kriging fleet on the ECDF score **and runs + ~2x faster** -- the per-infill model selection is not worth its cost in the low-data BO regime, + and the Exponential kernel's heavier tails generalize well across the difficulty spectrum + (Sphere/Rosenbrock/Rastrigin/RotatedEllipsoid). For per-problem kernel selection pass + ``surrogate=AutoModel(default_kriging())`` explicitly; any pysurrogate Model works. + """ + return Kriging(corr=Exponential()) + + +class BayesianOptimization(SurrogateAssistedAlgorithm): + # uses its own pysurrogate surrogate (fit lazily in _infill) -> skip the base build. + build_default_surrogate = False + + def __init__( + self, + acq_func=LogEI(), + optimizer=VectorizedGradientDescent(), + infill=None, + surrogate=None, + nth_optimize=10, + output=EGOOutput(), + **kwargs, + ): + + super().__init__(output=output, **kwargs) + self.default_termination = DefaultSingleObjectiveTermination() + + # optimizer: any pysamoo.experimental.optimizer.Optimizer instance, used by the default + # GlobalEI infill. VectorizedGradientDescent climbs EI by the surrogate's analytic + # mean/variance gradients. It requires an EI acquisition; for a non-EI one (POI/UCB) pass + # GeneticAlgorithm. + self.optimizer = optimizer + self.acq_func = acq_func + self.acq = None + + # infill_strategy: the pluggable "where to sample next" strategy + # (pysamoo.experimental.infill.Infill). Default Hybrid = global EI exploration that hands off + # to a local quadratic-Newton refinement once EI stalls (and back again), which drives the + # incumbent far deeper than pure global EI in higher dimensions. Pass GlobalEI(optimizer) for + # the plain global-EI step, or LocalQuadratic() for pure local refinement. (Named *_strategy + # to avoid shadowing pymoo Algorithm's own ``infill()`` method.) + self.infill_strategy = infill if infill is not None else Hybrid(GlobalEI(optimizer)) + + # surrogate: ANY pysurrogate Model -- it IS the surrogate, used directly via fit/predict. + # Default is model selection over the Kriging fleet (default_surrogate()); pass e.g. + # ``Kriging(corr=Gaussian())`` for a fixed, faster surrogate, or any other pysurrogate Model. + self.surrogate = surrogate if surrogate is not None else default_surrogate() + + # nth_optimize: the surrogate is fit ONCE on the DOE (which also runs model selection), then + # *refit* with the new points each infill. Re-optimizing theta every refit is wasteful, so + # only do it every nth refit (optimize=True) and otherwise refit cheaply (optimize=False). + # 1 = optimize every refit; None = never re-optimize after the first fit. + self.nth_optimize = nth_optimize + self._n_seen = 0 + self._n_refit = 0 + + # the fitted surrogate for the current infill (set lazily in _infill by get_model()). + self._model = None + + def _infill(self): + + # all evaluated points so far + X, F = self._archive.get("X", "F") + y = F[:, 0] + problem = self.problem + + # The surrogate is a pysurrogate Model used directly (fit/refit/predict on the object itself + # -- if it is an AutoModel it runs selection inside its fit). It is fit LAZILY via + # get_model() so a purely local infill step (LocalQuadratic) skips it. The FIRST call fits; + # later calls refit only the new points, re-optimizing theta every nth_optimize-th refit. + def get_model(): + if self._model is None: + self._model = self.surrogate.fit(X, y) + self._n_seen = len(X) + elif len(X) > self._n_seen: + self._n_refit += 1 + optimize = self.nth_optimize is not None and self._n_refit % self.nth_optimize == 0 + self.surrogate.refit(X[self._n_seen :], y[self._n_seen :], optimize=optimize) + self._n_seen = len(X) + return self._model + + # EI improves over the incumbent -- the best objective observed so far. + f_min = float(y.min()) + + # the pluggable infill strategy chooses the next point (GlobalEI fits + maximizes the + # acquisition; Hybrid/LocalQuadratic add a surrogate-free local quadratic-Newton step). + x_best, acq_val = self.infill_strategy.do(problem, get_model, X, y, self.acq_func, self.random_state) + + # AcquisitionProblem is kept only for the output/visualization, when a model was fit. + if self._model is not None: + self.acq = AcquisitionProblem(problem, self._model, self.acq_func, f_min=f_min) + return Population.new(X=x_best[None, :], acq=np.array([[acq_val]]))[[0]] + + def _set_optimum(self): + self.opt = FitnessSurvival().do(self.problem, self._archive, n_survive=1) + + +class EGOVisualization(Callback): + def notify(self, algorithm): + problem = algorithm.problem + if problem.n_var > 1 or problem.n_obj > 1 or algorithm._model is None: + return + + fig = plt.figure() + + gs = fig.add_gridspec(4, 1) + plt_func = fig.add_subplot(gs[:3]) + plt_acq = fig.add_subplot(gs[3]) + + X = algorithm.pop.get("X") + F = problem.evaluate(X) + infill = algorithm.infills[0] + plt_func.scatter(X, F, color="red") + acq = algorithm.acq + + mesh = np.linspace(problem.xl[0], problem.xu[0], 1000)[:, None] + + gp = algorithm._model + pred = gp.predict(mesh, var=True) + mu, sigma = pred.y, pred.sigma + + plt_func.fill_between(mesh[:, 0], (mu - 2 * sigma)[:, 0], (mu + 2 * sigma)[:, 0], alpha=0.2, color="k") + + plt_func.scatter(infill.X, infill.F, color="red", s=100, marker="x") + plt_func.plot(mesh, mu, color="red") + + plt_func.axvline(x=algorithm.infills[0].X, color="black", linestyle="dashed") + + plt_func.plot(mesh, problem.evaluate(mesh), color="black") + + plt_acq.plot(mesh, acq.evaluate(mesh), color="blue") + plt_acq.scatter(infill.X, acq.evaluate(infill.X), color="red", s=100, marker="x") + + plt.show() diff --git a/src/pysamoo/experimental/infill.py b/src/pysamoo/experimental/infill.py new file mode 100644 index 0000000..041328c --- /dev/null +++ b/src/pysamoo/experimental/infill.py @@ -0,0 +1,522 @@ +"""Pluggable infill strategies: where Bayesian optimization samples next (global / local / hybrid).""" + +import numpy as np + +from pysamoo.experimental.optimizer import VectorizedGradientDescent + + +class Infill: + """Decide the next point to evaluate, given the archive and the fitted surrogate. + + The single seam between the BO loop and *how* the next point is chosen. A strategy reads the + evaluated points ``X``/``F`` (and, if it wants, the surrogate ``model``) and returns the next + point plus a scalar acquisition value (for display). Concrete strategies: :class:`GlobalEI` + (explore the whole box), :class:`LocalQuadratic` (refine locally), :class:`Hybrid` (switch). + """ + + def do(self, problem, get_model, X, F, acq_func, random_state=None): + """Return ``(x, acq_value)`` -- the next point and its acquisition value (minimization). + + Args: + problem: The problem (used for its box bounds and dimensionality). + get_model: Zero-arg callable returning the fitted surrogate, fitting it lazily on first + call. Global strategies call it; purely local ones do not -- so the (expensive) GP + fit is skipped entirely on local steps. + X: Evaluated inputs, shape ``(n, d)``. + F: Evaluated objective values, shape ``(n,)``. + acq_func: The acquisition function (e.g. ``LogEI()``). + random_state: Optional generator threaded into any sampling. + + Returns: + Tuple ``(x, acq_value)`` -- the chosen point ``(d,)`` and its value (lower is better). + """ + raise NotImplementedError + + +class GlobalEI(Infill): + """Global exploration: maximize the acquisition over the whole box (the standard BO step). + + Thin wrapper around an acquisition :class:`~pysamoo.experimental.optimizer.Optimizer` that + supplies the incumbent ``f_min`` and the best-evaluated ``elites`` (for basin-local seeding). + + Args: + optimizer: The acquisition optimizer (default ``VectorizedGradientDescent``). + n_elite: How many best points to pass as seeding elites. + """ + + def __init__(self, optimizer=None, n_elite=5): + self.optimizer = optimizer if optimizer is not None else VectorizedGradientDescent() + self.n_elite = n_elite + + def do(self, problem, get_model, X, F, acq_func, random_state=None): + """Maximize the acquisition globally; see :meth:`Infill.do`.""" + model = get_model() # global EI needs the surrogate -> fit it + f_min = float(F.min()) + elites = X[np.argsort(F)[: self.n_elite]] + return self.optimizer.optimize(problem, model, acq_func, f_min, elites=elites, random_state=random_state) + + +def _quadratic_step(X, F, xc, f_min, lo, hi): + """Fit a quadratic to ``(X, F)`` around ``xc``; return its LM-Newton minimizer in ``[lo, hi]``. + + Least-squares fits ``f ~ a + g.s + 0.5 s'H s`` (``s = x - xc``) to the given points, then steps + to the model minimizer ``-H^-1 g`` with Levenberg-Marquardt regularization so an indefinite or + near-singular ``H`` still yields a descent step, clipped to the trust-region box. + + The model **degree falls back with the point budget** -- a full quadratic has + ``1 + d + d(d+1)/2`` (~d**2/2) coefficients, which a small archive cannot determine. When fewer + points than that are available it drops to a **diagonal** quadratic (squared terms only, + ``2d+1`` coefficients): still curvature-aware (so it keeps the Newton behavior, unlike a linear + model) and exact on separable objectives, just without cross-terms. It never drops below + quadratic. + + Args: + X: Nearby evaluated inputs, shape ``(m, d)``. + F: Their objective values, shape ``(m,)``. + xc: Center of the local model (the incumbent), shape ``(d,)``. + f_min: Objective at ``xc`` (for the predicted-decrease return). + lo: Lower bound of the trust-region box, shape ``(d,)``. + hi: Upper bound of the trust-region box, shape ``(d,)``. + + Returns: + Tuple ``(x_star, predicted_decrease)`` -- the step target and ``f_min - model(x_star)``. + """ + d = X.shape[1] + diff = X - xc + # full quadratic when the points can determine it; otherwise a diagonal quadratic (curvature + # without cross-terms, 2d+1 dof) -- never below quadratic. + full_pairs = [(i, j) for i in range(d) for j in range(i, d)] + pairs = full_pairs if len(X) >= 1 + d + len(full_pairs) else [(i, i) for i in range(d)] + design = np.column_stack( + [np.ones(len(X))] + [diff[:, i] for i in range(d)] + [diff[:, i] * diff[:, j] for i, j in pairs] + ) + coef, *_ = np.linalg.lstsq(design, F, rcond=None) + g = coef[1 : 1 + d] + hess = np.zeros((d, d)) + for k, (i, j) in enumerate(pairs): + v = coef[1 + d + k] + if i == j: + hess[i, i] = 2.0 * v + else: + hess[i, j] = hess[j, i] = v + # robust LM-Newton: nan-guard a degenerate lstsq fit, and use lstsq (pseudo-inverse) rather than + # solve so a singular/ill-conditioned Hessian never raises (seen on Griewank/Zakharov). + g, hess = np.nan_to_num(g), np.nan_to_num(hess) + lam = max(0.0, -np.linalg.eigvalsh(hess).min()) + 1e-6 + s = np.linalg.lstsq(hess + lam * np.eye(d), -g, rcond=None)[0] + x_star = np.clip(xc + s, lo, hi) + ds = x_star - xc + model_val = coef[0] + g @ ds + 0.5 * ds @ hess @ ds + return x_star, f_min - model_val + + +class LocalQuadratic(Infill): + """Local refinement: fit a quadratic to nearby points and take an LM-Newton trust-region step. + + Independent of the surrogate -- it approximates the objective *directly* from the nearest + evaluated points (a quadratic least-squares fit), which is exactly the dense local data the + biased BO sample provides near the optimum. It steps to that model's minimizer clipped to a box + of side ``L*span`` around the incumbent, and adapts ``L`` by the standard trust-region ratio + ``rho = actual decrease / predicted decrease``: shrink when the step under-delivers, grow when it + over-delivers. + + Args: + L0: Initial trust-region side as a fraction of the box width. + L_bounds: ``(min, max)`` clamp on the trust-region side. + eta: ``rho`` below this shrinks the region (a poor step). + eta_grow: ``rho`` above this grows it (a good step). + shrink: Multiplicative shrink factor. + expand: Multiplicative grow factor. + """ + + def __init__(self, L0=0.1, L_bounds=(1e-4, 0.5), eta=0.1, eta_grow=0.5, shrink=0.5, expand=2.0): + self.L0 = L0 + self.L_bounds = L_bounds + self.eta = eta + self.eta_grow = eta_grow + self.shrink = shrink + self.expand = expand + self.reset() + + def reset(self): + """Reset the trust region to its initial size and forget the last step (on a fresh local phase).""" + self._L = self.L0 + self._center = None + self._f_center = None + self._pred_dec = None + + def do(self, problem, get_model, X, F, acq_func, random_state=None): + """Take one local quadratic-Newton trust-region step; see :meth:`Infill.do` (ignores the surrogate).""" + xl, xu = problem.bounds() + span = xu - xl + d = problem.n_var + i = int(F.argmin()) + xc, f_min = X[i], float(F[i]) + + # adapt L from how the PREVIOUS local step actually performed vs. its prediction + if self._center is not None and self._pred_dec is not None: + rho = (self._f_center - f_min) / self._pred_dec if self._pred_dec > 1e-300 else -1.0 + lo_b, hi_b = self.L_bounds + if rho < self.eta: + self._L = max(self._L * self.shrink, lo_b) + elif rho > self.eta_grow: + self._L = min(self._L * self.expand, hi_b) + + half = 0.5 * self._L * span + lo, hi = np.maximum(xc - half, xl), np.minimum(xc + half, xu) + dof = 1 + d + d * (d + 1) // 2 # quadratic degrees of freedom + inside = np.where(np.all((X >= lo) & (X <= hi), axis=1))[0] + idx = inside if len(inside) >= dof + 1 else np.argsort(((X - xc) ** 2).sum(1))[: dof + 1] + + x, pred_dec = _quadratic_step(X[idx], F[idx], xc, f_min, lo, hi) + self._center, self._f_center, self._pred_dec = xc, f_min, pred_dec + return x, -pred_dec + + +class Hybrid(Infill): + """Switch between :class:`GlobalEI` (explore) and :class:`LocalQuadratic` (refine) on a stall. + + Runs the global strategy until it fails to improve the incumbent for ``patience_global`` + consecutive infills (EI exploration is spent), then hands off to the local strategy; when the + local strategy stalls for ``patience_local`` (its trust region has collapsed), it switches back + to global to explore elsewhere. The mode + stall counter live here; each sub-strategy owns its + own state (surrogate/EI for global, trust region for local). + + Args: + global_strategy: The exploration strategy (default :class:`GlobalEI`). + local_strategy: The refinement strategy (default :class:`LocalQuadratic`). + patience_global: Consecutive non-improving global infills before switching to local. + patience_local: Consecutive non-improving local infills before switching back to global. + """ + + def __init__(self, global_strategy=None, local_strategy=None, patience_global=4, patience_local=5): + self.g = global_strategy if global_strategy is not None else GlobalEI() + self.l = local_strategy if local_strategy is not None else LocalQuadratic() + self.patience_global = patience_global + self.patience_local = patience_local + self.mode = "global" + self.stall = 0 + self._prev_best = None + + def do(self, problem, get_model, X, F, acq_func, random_state=None): + """Pick the active strategy (flipping on a stall) and delegate; see :meth:`Infill.do`.""" + best = float(F.min()) + if self._prev_best is not None: + improved = best < self._prev_best - 1e-12 * max(1.0, abs(best)) + self.stall = 0 if improved else self.stall + 1 + patience = self.patience_global if self.mode == "global" else self.patience_local + if self.stall >= patience: + self.mode = "local" if self.mode == "global" else "global" + self.stall = 0 + if self.mode == "local": + self.l.reset() + self._prev_best = best + + active = self.g if self.mode == "global" else self.l + return active.do(problem, get_model, X, F, acq_func, random_state) + + +def _weighted_pca(X, F, xc): + """Weighted-PCA rotation of points around ``xc``, weighted toward better objective values. + + Computes the rotation ``R`` whose columns are the principal axes of the quality-weighted local + point cloud -- the LABCAT idea: rotate so the (rotated) valley becomes axis-aligned, letting a + cheap *diagonal* model capture an ill-conditioned, rotated valley with only ``2d+1`` parameters + instead of a full quadratic's ``~d^2/2``. + + Args: + X: Nearby evaluated inputs, shape ``(m, d)``. + F: Their objective values, shape ``(m,)``. + xc: Center (the incumbent), shape ``(d,)``. + + Returns: + Orthonormal rotation ``R``, shape ``(d, d)`` (so ``z = R^T (x - xc)``). + """ + spread = float(F.max() - F.min()) + w = np.exp(-(F - F.min()) / (spread + 1e-12)) if spread > 0 else np.ones(len(F)) + w = w / w.sum() + diff = X - xc + cov = (diff * w[:, None]).T @ diff + cov = cov + 1e-12 * np.eye(X.shape[1]) + _, R = np.linalg.eigh(cov) + return R + + +def _rotated_diag_step(X, F, xc, f_min, R, lo, hi): + """Fit a diagonal quadratic in the PCA-rotated frame and return its LM-Newton minimizer. + + In the rotated frame ``z = R^T (x - xc)`` the model is ``f ~ a + g.z + 0.5 sum_j h_j z_j^2`` + (``2d+1`` coefficients): curvature-aware but cheap, and -- because the frame is aligned with the + local principal axes -- able to represent a *rotated* ellipsoidal valley that an axis-aligned + diagonal model cannot. The step is the Levenberg-Marquardt-regularized Newton minimizer, mapped + back to the original space and clipped to the box. + + Args: + X: Nearby evaluated inputs, shape ``(m, d)``. + F: Their objective values, shape ``(m,)``. + xc: Center (the incumbent), shape ``(d,)``. + f_min: Objective at ``xc`` (for the predicted-decrease return). + R: Rotation from :func:`_weighted_pca`, shape ``(d, d)``. + lo: Lower bound of the trust-region box, shape ``(d,)``. + hi: Upper bound of the trust-region box, shape ``(d,)``. + + Returns: + Tuple ``(x_star, predicted_decrease)`` -- the step target and ``f_min - model(x_star)``. + """ + d = X.shape[1] + z = (X - xc) @ R + design = np.column_stack([np.ones(len(z))] + [z[:, j] for j in range(d)] + [z[:, j] ** 2 for j in range(d)]) + coef, *_ = np.linalg.lstsq(design, F, rcond=None) + g = np.nan_to_num(coef[1 : 1 + d]) + h = np.nan_to_num(2.0 * coef[1 + d : 1 + 2 * d]) + lam = max(0.0, -h.min()) + 1e-6 + s = -g / (h + lam) + x_star = np.clip(xc + R @ s, lo, hi) + s_eff = R.T @ (x_star - xc) + model_val = coef[0] + g @ s_eff + 0.5 * np.sum(h * s_eff**2) + return x_star, f_min - model_val + + +class LocalPCAQuadratic(Infill): + """Local refinement for ROTATED, ill-conditioned valleys (LABCAT-style PCA rotation). + + Like :class:`LocalQuadratic` but fits its quadratic in the quality-weighted PCA frame of the + nearby points, where a cheap *diagonal* quadratic (``2d+1`` dof) can model a rotated ellipsoidal + valley -- so it fits far earlier than a full quadratic (``~d^2/2`` dof) and stays + well-conditioned. Surrogate-free (never calls ``get_model``), so it is ~free. The trust region + adapts by the standard ``rho`` ratio. + + Args: + L0: Initial trust-region side as a fraction of the box width. + L_bounds: ``(min, max)`` clamp on the trust-region side. + eta: ``rho`` below this shrinks the region. + eta_grow: ``rho`` above this grows it. + shrink: Multiplicative shrink factor. + expand: Multiplicative grow factor. + """ + + def __init__(self, L0=0.1, L_bounds=(1e-4, 0.5), eta=0.1, eta_grow=0.5, shrink=0.5, expand=2.0): + self.L0 = L0 + self.L_bounds = L_bounds + self.eta = eta + self.eta_grow = eta_grow + self.shrink = shrink + self.expand = expand + self.reset() + + def reset(self): + """Reset the trust region and forget the last step (on a fresh local phase).""" + self._L = self.L0 + self._center = None + self._f_center = None + self._pred_dec = None + + def do(self, problem, get_model, X, F, acq_func, random_state=None): + """Take one PCA-rotated diagonal quadratic-Newton trust-region step; see :meth:`Infill.do`.""" + xl, xu = problem.bounds() + span = xu - xl + d = problem.n_var + i = int(F.argmin()) + xc, f_min = X[i], float(F[i]) + + if self._center is not None and self._pred_dec is not None: + rho = (self._f_center - f_min) / self._pred_dec if self._pred_dec > 1e-300 else -1.0 + lo_b, hi_b = self.L_bounds + if rho < self.eta: + self._L = max(self._L * self.shrink, lo_b) + elif rho > self.eta_grow: + self._L = min(self._L * self.expand, hi_b) + + half = 0.5 * self._L * span + lo, hi = np.maximum(xc - half, xl), np.minimum(xc + half, xu) + dof = 2 * d + 1 # diagonal-quadratic degrees of freedom + inside = np.where(np.all((X >= lo) & (X <= hi), axis=1))[0] + idx = inside if len(inside) >= dof + 1 else np.argsort(((X - xc) ** 2).sum(1))[: dof + 1] + + R = _weighted_pca(X[idx], F[idx], xc) + x, pred_dec = _rotated_diag_step(X[idx], F[idx], xc, f_min, R, lo, hi) + self._center, self._f_center, self._pred_dec = xc, f_min, pred_dec + return x, -pred_dec + + +class RestartingLocal(Infill): + """Multi-start local search for MULTIMODAL functions (escape basins, e.g. Rastrigin). + + Runs a cheap local refiner until it stalls (no incumbent improvement for ``patience`` calls), + then restarts it from a space-filling point chosen FAR from all evaluated points (farthest-point + / max-min over a random candidate pool). Many cheap local dives at diverse locations find the + global basin among exponentially many local minima. Surrogate-free. + + Args: + local_strategy: The local refiner to restart (default :class:`LocalQuadratic`). + patience: Consecutive non-improving calls before a restart. + pool: Candidate pool size for the farthest-point restart pick. + """ + + def __init__(self, local_strategy=None, patience=4, pool=200): + self.local = local_strategy if local_strategy is not None else LocalQuadratic() + self.patience = patience + self.pool = pool + self.stall = 0 + self._prev_best = None + + def reset(self): + """Reset the stall counter and the wrapped local refiner (on a fresh local phase).""" + self.stall = 0 + self._prev_best = None + self.local.reset() + + def do(self, problem, get_model, X, F, acq_func, random_state=None): + """Refine locally, restarting from a far point on a stall; see :meth:`Infill.do`.""" + xl, xu = problem.bounds() + rng = random_state if random_state is not None else np.random.default_rng(0) + best = float(F.min()) + if self._prev_best is not None: + improved = best < self._prev_best - 1e-12 * max(1.0, abs(best)) + self.stall = 0 if improved else self.stall + 1 + self._prev_best = best + + if self.stall >= self.patience: + self.stall = 0 + self.local.reset() + cand = xl + rng.random((self.pool, problem.n_var)) * (xu - xl) + # farthest-point: maximize min distance to all evaluated points + d2 = ((cand[:, None, :] - X[None, :, :]) ** 2).sum(-1).min(axis=1) + x = cand[int(np.argmax(d2))] + return x, 0.0 + return self.local.do(problem, get_model, X, F, acq_func, random_state) + + +class Explorer(Infill): + """Pure space-filling exploration -- never exploits, just keeps covering the box. + + Each infill draws a fresh Latin-Hypercube batch that is spaced *away from the current + population* via pysampling's ``LatinHypercubeSampling(Xp=...)`` -- the ``Xp`` argument folds the + distance to the already-evaluated points into the maximin energy, so the new design fills the + emptiest region ("LHS conditioned on ``Xp``"). It returns the most novel of the batch (largest + nearest-neighbour gap). There is no incumbent, no acquisition, no surrogate call -- it is the + exploration extreme (the opposite of :class:`LocalQuadratic`). + + Two uses. (1) A baseline / diversification component. (2) A *measurement* tool: because the + design is unbiased space-filling (not the self-selected, clustered sample an EI loop produces), + the surrogate's prequential refit score under Explorer is an honest learning curve for *how + fast a model learns the function* -- the quantity a normal BO run confounds with where it chose + to look (see ``findings.md`` §8). + + Args: + batch: Number of LHS points drawn per infill (spaced against ``Xp``); the most novel is returned. + criterion: pysampling LHS criterion (``"maxmin"`` optimizes spacing incl. distance to ``Xp``). + """ + + def __init__(self, batch=16, criterion="maxmin"): + self.batch = batch + self.criterion = criterion + + def do(self, problem, get_model, X, F, acq_func, random_state=None): + """Return the most-novel point of an LHS batch drawn away from ``X`` (via ``Xp``); see :meth:`Infill.do`.""" + from pysampling.algorithms.lhs import LatinHypercubeSampling + + xl, xu = problem.bounds() + span = xu - xl + rng = random_state if random_state is not None else np.random.default_rng(0) + # existing population in the unit cube -> pysampling samples the new batch AWAY from it + Xp = (X - xl) / span + U = LatinHypercubeSampling(criterion=self.criterion, Xp=Xp, random_state=rng).sample(self.batch, problem.n_var) + cand = xl + U * span + # return the candidate whose nearest already-evaluated neighbour is farthest away + d2 = ((cand[:, None, :] - X[None, :, :]) ** 2).sum(-1).min(axis=1) + j = int(np.argmax(d2)) + # acq value (lower = better, for display): negative fill distance -- bigger gap = more novel + return cand[j], -float(np.sqrt(d2[j])) + + +class LocalCMAES(Infill): + """Sequential CMA-ES local strategy: learns the metric (covariance ~ inverse Hessian). + + The derivative-free method of choice for ill-conditioned, non-separable, curved-valley problems + (Rosenbrock, RotatedEllipsoid): it adapts a full covariance ``C`` so the sampling distribution + aligns with the local valley, learning the conditioning/rotation a diagonal model cannot. Runs + one sample per infill -- it draws ``x = mean + sigma * B D z`` (``z ~ N(0, I)``), and once a full + generation of ``lambda`` points has been evaluated, applies the standard CMA-ES rank-mu / rank-one + update (Hansen) to ``mean``, ``sigma``, and ``C``. Surrogate-free (never calls ``get_model``). + + Designed for *pure* use (every infill is its own); inside :class:`Hybrid` the generation + bookkeeping (which archive rows are this generation's) is only approximate. + + Args: + sigma0: Initial step size as a fraction of the mean box width. + """ + + def __init__(self, sigma0=0.3): + self.sigma0 = sigma0 + self.reset() + + def reset(self): + """Forget the CMA state so the next call re-initializes from the current incumbent.""" + self._init = False + self._gen = 0 + self._returned = [] + + def _setup(self, d, xl, xu, xc): + """Initialize CMA-ES constants and state for dimension ``d`` centered at ``xc``.""" + self.d = d + self.lam = 4 + int(3 * np.log(d)) + self.mu = self.lam // 2 + w = np.log(self.mu + 0.5) - np.log(np.arange(1, self.mu + 1)) + self.w = w / w.sum() + self.mueff = 1.0 / np.sum(self.w**2) + self.cs = (self.mueff + 2) / (d + self.mueff + 5) + self.ds = 1 + 2 * max(0.0, np.sqrt((self.mueff - 1) / (d + 1)) - 1) + self.cs + self.cc = (4 + self.mueff / d) / (d + 4 + 2 * self.mueff / d) + self.c1 = 2 / ((d + 1.3) ** 2 + self.mueff) + self.cmu = min(1 - self.c1, 2 * (self.mueff - 2 + 1 / self.mueff) / ((d + 2) ** 2 + self.mueff)) + self.chiN = np.sqrt(d) * (1 - 1 / (4 * d) + 1 / (21 * d**2)) + self.ps = np.zeros(d) + self.pc = np.zeros(d) + self.C = np.eye(d) + self.B = np.eye(d) + self.D = np.ones(d) + self.mean = xc.copy().astype(float) + self.sigma = self.sigma0 * float(np.mean(xu - xl)) + self._init = True + + def _update(self, Xg, Fg): + """Apply the CMA-ES update from a finished generation's points ``Xg`` and values ``Fg``.""" + order = np.argsort(Fg) + ys = (Xg - self.mean) / self.sigma + ysel = ys[order[: self.mu]] + yw = self.w @ ysel + self.mean = self.mean + self.sigma * yw + cinv = self.B @ np.diag(1.0 / self.D) @ self.B.T + self.ps = (1 - self.cs) * self.ps + np.sqrt(self.cs * (2 - self.cs) * self.mueff) * (cinv @ yw) + self.sigma *= np.exp((self.cs / self.ds) * (np.linalg.norm(self.ps) / self.chiN - 1)) + denom = np.sqrt(1 - (1 - self.cs) ** (2 * (self._gen + 1))) + hs = 1.0 if np.linalg.norm(self.ps) / denom < (1.4 + 2 / (self.d + 1)) * self.chiN else 0.0 + self.pc = (1 - self.cc) * self.pc + hs * np.sqrt(self.cc * (2 - self.cc) * self.mueff) * yw + rank_mu = sum(self.w[k] * np.outer(ysel[k], ysel[k]) for k in range(self.mu)) + self.C = ( + (1 - self.c1 - self.cmu) * self.C + + self.c1 * (np.outer(self.pc, self.pc) + (1 - hs) * self.cc * (2 - self.cc) * self.C) + + self.cmu * rank_mu + ) + self.C = np.triu(self.C) + np.triu(self.C, 1).T + vals, self.B = np.linalg.eigh(self.C) + self.D = np.sqrt(np.clip(vals, 1e-20, None)) + + def do(self, problem, get_model, X, F, acq_func, random_state=None): + """Draw the next CMA-ES sample, updating the distribution after each full generation.""" + xl, xu = problem.bounds() + d = problem.n_var + rng = random_state if random_state is not None else np.random.default_rng(0) + if not self._init: + self._setup(d, xl, xu, X[int(F.argmin())]) + # once a full generation of lambda points has been returned (and so evaluated -- they are the + # last lambda archive rows in pure use), apply the CMA-ES update and start a new generation. + if len(self._returned) == self.lam: + self._update(np.array(self._returned), F[-self.lam :]) + self._returned = [] + self._gen += 1 + z = rng.standard_normal(d) + x = np.clip(self.mean + self.sigma * (self.B @ (self.D * z)), xl, xu) + self._returned.append(x) + return x, 0.0 diff --git a/src/pysamoo/experimental/optimizer.py b/src/pysamoo/experimental/optimizer.py new file mode 100644 index 0000000..70eaca1 --- /dev/null +++ b/src/pysamoo/experimental/optimizer.py @@ -0,0 +1,209 @@ +"""Pluggable acquisition-function optimizers for Bayesian optimization. + +An *acquisition optimizer* maximizes an acquisition function (e.g. Expected +Improvement) over a problem's box to choose the next point to evaluate. The base +:class:`Optimizer` defines a single ``optimize`` method; concrete strategies +implement it, and :class:`~pysamoo.experimental.bo.BayesianOptimization` accepts any +:class:`Optimizer` instance. + +Built-in strategies: + +* :class:`VectorizedGradientDescent` -- the default. Maximizes EI with pysurrogate's + generic :class:`~pysurrogate.optimizer.Adam` over an + :class:`~pysamoo.experimental.acquisition.EIProblem`: a population climbs ``-EI`` by the + surrogate's analytic mean/variance gradients, one batched ``predict(grad=True)`` per step. + Fast and seed-independent on a well-fit surrogate -- no maximin seed pool needed. +* :class:`GeneticAlgorithm` -- derivative-free niching GA over a (plain) LHS seed. Robust on + any model and any acquisition; the fallback when the acquisition is not EI. + +``optimize`` returns the chosen point and the acquisition value there, in the *minimization* +convention that matches :class:`~pysamoo.experimental.acquisition.AcquisitionProblem` (lower is +better, i.e. ``-EI``). +""" + +import numpy as np +from pymoo.algorithms.soo.nonconvex.ga_niching import NicheGA +from pymoo.optimize import minimize as pymoo_minimize +from pymoo.termination.default import DefaultSingleObjectiveTermination +from pysampling.algorithms.lhs import LatinHypercubeSampling +from pysurrogate.core.sampling import Sampling +from pysurrogate.optimizer import Adam + +from pysamoo.experimental.acquisition import EI, AcquisitionProblem, EIProblem, LogEI + + +def _seed_pool(sampling, problem, n, random_state=None): + """Draw a seed pool in the problem's box with a pysampling ``Sampling`` object. + + The ``Sampling`` instance is the pluggable hook: the default + ``LatinHypercubeSampling(criterion=None)`` is a single plain Latin-hypercube draw in + ``[0, 1]^d`` -- cheap, because these are only *starting points* for the optimizer and + do not need an optimized space-filling layout. A maximin-optimized draw + (``LatinHypercubeSampling(criterion="maxmin")``, the pysampling default) re-runs a 20-sweep + swap search every call, which dominated the BO runtime for no quality gain; pass it (or + ``RieszEnergySampling``) only when seed spacing specifically matters. The unit draw is + scaled to the box. + + Args: + sampling: A pysampling ``Sampling`` instance producing points in ``[0, 1]^d``. + problem: The problem, used for its dimensionality and box bounds. + n: Number of seed points to draw. + random_state: Optional seed/generator threaded into the draw for reproducibility; + set on the sampler so successive infills advance a shared generator. + + Returns: + The seed points, shape ``(n, d)``, within ``[xl, xu]``. + """ + xl, xu = problem.bounds() + sampling.random_state = random_state + unit = sampling.sample(n, problem.n_var) + return xl + unit * (xu - xl) + + +def _seed_from(random_state): + """Coerce a pymoo ``random_state`` (int, numpy ``Generator``, or ``None``) to an int seed.""" + if random_state is None: + return 0 + if isinstance(random_state, (int, np.integer)): + return int(random_state) + # a numpy Generator / RandomState: draw a reproducible integer seed from it + return int(random_state.integers(0, 2**31 - 1)) + + +class Optimizer: + """Base class for acquisition optimizers: maximize an acquisition over the box.""" + + def optimize(self, problem, model, acq_func, f_min, elites=None, random_state=None): + """Choose the next point by maximizing the acquisition over the problem's box. + + Args: + problem: The (real) problem, used only for its box bounds. + model: The surrogate, exposing ``predict`` (and ``predict(grad=True)`` for the + gradient strategies). + acq_func: The acquisition function (e.g. ``EI()``). + f_min: The incumbent target the acquisition improves over. + elites: Optional best evaluated points ``(k, d)``; the search adds a small *perturbed* + cloud around them to its seeds, so it starts in the (well-sampled, low-uncertainty) + basin where the EI peak lives but a global draw never lands. ``None`` seeds globally + only. + random_state: Optional generator threaded into any sampling. + + Returns: + Tuple ``(x_best, acq_value)`` -- the chosen point ``(d,)`` and the + acquisition value there in minimization convention (``-EI`` for EI). + """ + raise NotImplementedError + + +class VectorizedGradientDescent(Optimizer): + """Maximize (log)EI with pysurrogate's population :class:`~pysurrogate.optimizer.Adam`. + + Builds an :class:`~pysamoo.experimental.acquisition.EIProblem` (``-log EI`` over the unit cube, + with the analytic gradient from the surrogate) and hands it to pysurrogate's generic Adam -- + the same optimizer layer that fits the kriging hyper-parameters. A population of ``pop_size`` + points climbs EI together via one batched ``predict(grad=True)`` per step. + + EI is multimodal (a sharp exploit peak in the well-sampled incumbent basin plus broad explore + bumps far from data), so the population must be *seeded* in the promising basins or Adam gets + lost on a shallow far-away gradient. The seeding is a cheap screen: draw ``n_pool`` points from + a plain pysampling LHS (``criterion=None`` -- no maximin, so it stays fast), **plus a small + perturbed cloud around the best evaluated points** (``elites``) -- because the EI peak sits next + to the incumbent, in a basin so small a global draw never lands there (seeding the incumbent + *exactly* is useless: sigma=0 there so EI and its gradient vanish, hence the perturbation). The + combined pool is ranked by a value-only ``EIProblem.screen`` and Adam starts from the best + ``pop_size``. No gradient is spent on the pool, only on the population that climbs. + + Args: + pop_size: Number of points climbing EI in parallel (the Adam population). + n_iter: Number of Adam steps. + lr: Adam learning rate, a fraction of each dimension's box width (the EIProblem searches + the unit cube, so the rate is scale-free). + n_pool: Size of the plain-LHS global screen pool the starts are picked from. + n_elite: Number of best evaluated points to seed a local cloud around. + n_local: Points per elite cloud (split across a few perturbation scales). + sampling: pysampling ``Sampling`` for the global pool (default + ``LatinHypercubeSampling(criterion=None)``, a cheap plain draw). + """ + + # unit-cube std-devs of the local clouds: multi-scale so at least one band lands in the basin + # whatever its size (the improvement basin shrinks as the incumbent improves). + _LOCAL_SCALES = (0.01, 0.03, 0.1) + + def __init__(self, pop_size=32, n_iter=30, lr=0.1, n_pool=256, n_elite=3, n_local=30, sampling=None): + self.pop_size = pop_size + self.n_iter = n_iter + self.lr = lr + self.n_pool = n_pool + self.n_elite = n_elite + self.n_local = n_local + self.sampling = sampling if sampling is not None else LatinHypercubeSampling(criterion=None) + + def optimize(self, problem, model, acq_func, f_min, elites=None, random_state=None): + """Maximize (log)EI with screened-seed pysurrogate Adam; see :meth:`Optimizer.optimize`.""" + if not isinstance(acq_func, (EI, LogEI)): + raise TypeError("VectorizedGradientDescent supports only the EI / LogEI acquisition") + xl, xu = problem.bounds() + span = np.where(xu - xl == 0.0, 1.0, xu - xl) + ei = EIProblem(model, f_min, xl, xu, log=isinstance(acq_func, LogEI)) + + # plain LHS pool in the unit cube (no maximin), scored by the CURRENT model's value-only + # (log)EI -- recomputed every infill since the surrogate (and so EI) changes each time. + self.sampling.random_state = random_state + pool = self.sampling.sample(self.n_pool, problem.n_var) + + # local clouds around the best evaluated points: the EI peak is in the (densely sampled) + # incumbent basin, which the global LHS misses; perturbed seeds put Adam inside it. + if elites is not None and self.n_elite > 0 and self.n_local > 0: + rng = np.random.default_rng(_seed_from(random_state)) + ue = np.clip((np.atleast_2d(elites)[: self.n_elite] - xl) / span, 0.0, 1.0) + per = max(1, self.n_local // len(self._LOCAL_SCALES)) + clouds = [ + np.clip(u + rng.normal(0.0, s, (per, problem.n_var)), 0.0, 1.0) for u in ue for s in self._LOCAL_SCALES + ] + pool = np.vstack([pool, *clouds]) + + starts = pool[np.argsort(ei.screen(pool))[: self.pop_size]] + seed = Sampling(self.pop_size, include=list(starts)) + adam = Adam(steps=self.n_iter, lr=self.lr, sampling=seed, random_state=_seed_from(random_state)) + res = adam.minimize(ei) + u_best = res.x if res.x is not None else starts[0] + return ei.to_x(u_best)[0], float(res.f) + + +class GeneticAlgorithm(Optimizer): + """Niching genetic algorithm over a plain Latin-hypercube seed. + + Derivative-free and batched; works with any surrogate and any acquisition. The fallback for + a non-EI acquisition (POI, UCB), where the gradient strategy does not apply -- at the cost of + not pinpointing sharp optima as precisely as the gradient path. + + Args: + pop_size: Niching-GA population size. + n_sample: Size of the Latin-hypercube seed the GA is initialized from. + sampling: pysampling ``Sampling`` instance for the seed pool (default + ``LatinHypercubeSampling(criterion=None)``, a cheap plain draw); the pluggable hook. + """ + + def __init__(self, pop_size=50, n_sample=500, sampling=None): + self.pop_size = pop_size + self.n_sample = n_sample + self.sampling = sampling if sampling is not None else LatinHypercubeSampling(criterion=None) + + def optimize(self, problem, model, acq_func, f_min, elites=None, random_state=None): + """Maximize the acquisition with a niching GA; see :meth:`Optimizer.optimize`.""" + acq = AcquisitionProblem(problem, model, acq_func, f_min=f_min) + seeds = _seed_pool(self.sampling, problem, self.n_sample, random_state=random_state) + if elites is not None: + # let the niching GA also start from the best evaluated points + seeds = np.vstack([np.atleast_2d(elites), seeds]) + algorithm = NicheGA(pop_size=self.pop_size, sampling=seeds) + # thread the run's RNG into the inner GA: without a seed pymoo falls back to an unseeded + # default_rng(), making the acquisition optimum -- and thus the whole run -- irreproducible. + res = pymoo_minimize( + acq, + algorithm, + DefaultSingleObjectiveTermination(period=1), + seed=_seed_from(random_state), + verbose=False, + ) + return res.opt.get("X")[0], float(res.opt.get("F")[0, 0]) diff --git a/src/pysamoo/experimental/problems.py b/src/pysamoo/experimental/problems.py new file mode 100644 index 0000000..381e58e --- /dev/null +++ b/src/pysamoo/experimental/problems.py @@ -0,0 +1,78 @@ +"""Benchmark problems for the BO method search (ORACLE -- do not edit during method iteration).""" + +import numpy as np +from pymoo.core.problem import Problem +from pymoo.problems.single import Ackley, Griewank, Rastrigin, Rosenbrock, Sphere, Zakharov + + +class RotatedEllipsoid(Problem): + """Ill-conditioned, non-separable quadratic: the clean test of metric/rotation learning. + + ``f(x) = sum_i cond^(i/(d-1)) * z_i^2`` with ``z = R (x - x*)`` for a fixed random rotation + ``R``. Unlike Rosenbrock it has a *straight* (not curved) valley, so it isolates the + ill-conditioning + rotation difficulty without the curved-valley confound -- the pure test of + whether a method needs a Mahalanobis/PCA-rotated kernel. Optimum ``F*=0`` at ``x*``. + + Args: + n_var: Dimensionality. + cond: Condition number (ratio of largest to smallest axis stiffness). + seed: Seed for the fixed rotation and optimum location. + """ + + def __init__(self, n_var=2, cond=1e6, seed=0): + super().__init__(n_var=n_var, n_obj=1, xl=-5.0, xu=5.0) + rng = np.random.default_rng(seed) + q, _ = np.linalg.qr(rng.standard_normal((n_var, n_var))) + self.R = q + # optimum placed off-center but well inside the box so it is not trivially at a corner/center + self.x_opt = rng.uniform(-2.0, 2.0, size=n_var) + exps = np.arange(n_var) / max(n_var - 1, 1) + self.coef = cond**exps + + def _evaluate(self, x, out, *args, **kwargs): + z = (x - self.x_opt) @ self.R.T + out["F"] = (z**2 * self.coef).sum(axis=1, keepdims=True) + + +def benchmark_problems(dims=(2, 10)): + """Return the fixed benchmark suite as ``[(name, problem), ...]`` (optimum F*=0 for all). + + Four functions spanning the difficulty axes -- Sphere (baseline), Rosenbrock (curved-valley + local), Rastrigin (multimodal-funnel global), RotatedEllipsoid (conditioning + rotation) -- + each at the requested dimensionalities. + + Args: + dims: Dimensionalities to instantiate each function at. + + Returns: + List of ``(name, pymoo Problem)`` tuples. + """ + out = [] + for d in dims: + out.append((f"sphere_{d}d", Sphere(n_var=d))) + out.append((f"rosenbrock_{d}d", Rosenbrock(n_var=d))) + out.append((f"rastrigin_{d}d", Rastrigin(n_var=d))) + out.append((f"rot_ellipsoid_{d}d", RotatedEllipsoid(n_var=d, seed=d))) + return out + + +def heldout_problems(dims=(2, 10)): + """Return a held-out validation suite (functions NOT tuned against) to catch overfitting. + + The method search optimizes the four ``benchmark_problems``; a method can quietly *overfit* to + them. These three functions (Ackley, Griewank, Zakharov) are never tuned against -- a real + improvement must hold here too. (Lesson learned the hard way: a benchmark-only +12% ECDF gain + evaporated on held-out functions; only changes that improve both are kept.) + + Args: + dims: Dimensionalities to instantiate each function at. + + Returns: + List of ``(name, pymoo Problem)`` tuples. + """ + out = [] + for d in dims: + out.append((f"ackley_{d}d", Ackley(n_var=d))) + out.append((f"griewank_{d}d", Griewank(n_var=d))) + out.append((f"zakharov_{d}d", Zakharov(n_var=d))) + return out diff --git a/src/pysamoo/sampling/__init__.py b/src/pysamoo/sampling/__init__.py new file mode 100644 index 0000000..8fde1a6 --- /dev/null +++ b/src/pysamoo/sampling/__init__.py @@ -0,0 +1 @@ +"""Constrained sampling strategies for initial design generation.""" diff --git a/src/pysamoo/sampling/energy.py b/src/pysamoo/sampling/energy.py new file mode 100644 index 0000000..4903489 --- /dev/null +++ b/src/pysamoo/sampling/energy.py @@ -0,0 +1,53 @@ +"""Energy-based constrained sampling.""" + +import warnings + +import numpy as np +from pymoo.core.sampling import Sampling +from pymoo.util.normalization import denormalize, normalize +from pymoo.util.ref_dirs.energy import calc_potential_energy_with_grad +from pymoo.util.ref_dirs.optimizer import Adam + +from pysamoo.sampling.niching import NichingConstrainedSampling +from pysamoo.sampling.rejection import RejectionConstrainedSampling + + +class EnergyConstrainedSampling(Sampling): + def __init__(self, func_eval_constr, n_max_iter=10000): + super().__init__() + self.func_eval_constr = func_eval_constr + self.n_max_iter = n_max_iter + + def _do(self, problem, n_samples, random_state=None, **kwargs): + xl, xu = problem.bounds() + constr = self.func_eval_constr + d = problem.n_var**2 + + X = RejectionConstrainedSampling(constr).do(problem, n_samples, random_state=random_state).get("X") + if len(X) < n_samples: + X = NichingConstrainedSampling(constr).do(problem, n_samples, random_state=random_state).get("X") + + if len(X) == 0: + raise RuntimeError("No feasible solution could be found!") + elif len(X) < n_samples: + warnings.warn("Fewer feasible solutions than requested could be found.", stacklevel=2) + + X = normalize(X, xl, xu) + + optimizer = Adam(alpha=0.005) + + _, grad = calc_potential_energy_with_grad(X, d) + + # a fixed number of energy-minimization steps (the potential energy is monotone under Adam here) + for _ in range(self.n_max_iter): + _X = optimizer.next(X, grad) + _CV = constr(denormalize(_X, xl, xu)) + feasible = np.logical_and(_CV <= 0, np.all(np.logical_and(_X >= 0, _X <= 1), axis=1)) + + X[feasible] = _X[feasible] + + _, grad = calc_potential_energy_with_grad(X, d) + + X = denormalize(X, xl, xu) + + return X diff --git a/pysamoo/sampling/niching.py b/src/pysamoo/sampling/niching.py similarity index 80% rename from pysamoo/sampling/niching.py rename to src/pysamoo/sampling/niching.py index 214e978..66d8b7a 100644 --- a/pysamoo/sampling/niching.py +++ b/src/pysamoo/sampling/niching.py @@ -1,3 +1,5 @@ +"""Niching-based constrained sampling.""" + from pymoo.algorithms.soo.nonconvex.ga_niching import NicheGA from pymoo.core.meta import Meta from pymoo.core.sampling import Sampling @@ -7,22 +9,20 @@ class NichingConstrainedSampling(Sampling): - - def __init__(self, func_eval_constr, sampling=LHS(), initial_eps=0.25): + def __init__(self, func_eval_constr, sampling=None, initial_eps=0.25): super().__init__() self.func_eval_constr = func_eval_constr self.initial_eps = initial_eps - self.sampling = sampling + self.sampling = sampling if sampling is not None else LHS() def _do(self, problem, n_samples, **kwargs): constr = self.func_eval_constr class ConstrainedProblem(Meta): - def __init__(self, problem): super().__init__(problem) self.n_obj = 1 - self.n_constr = 1 + self.n_ieq_constr = 1 def _evaluate(self, x, out, *args, **kwargs): cv = constr(x) @@ -34,16 +34,13 @@ def _evaluate(self, x, out, *args, **kwargs): eps = self.initial_eps while True: - - algorithm = NicheGA(pop_size=n_samples, samping=self.sampling, norm_niche_size=eps, norm_by_dim=True) + algorithm = NicheGA(pop_size=n_samples, sampling=self.sampling, norm_niche_size=eps, norm_by_dim=True) res = minimize(problem, algorithm, ("n_gen", 200), return_least_infeasible=True) opt = res.opt X = opt.get("X")[opt.get("CV")[:, 0] <= 0] - print(eps, len(X)) - if len(X) >= n_samples: break diff --git a/pysamoo/sampling/rejection.py b/src/pysamoo/sampling/rejection.py similarity index 56% rename from pysamoo/sampling/rejection.py rename to src/pysamoo/sampling/rejection.py index 8f25bca..3ec0a92 100644 --- a/pysamoo/sampling/rejection.py +++ b/src/pysamoo/sampling/rejection.py @@ -1,20 +1,21 @@ -import numpy as np +"""Rejection-based constrained sampling with maximum-distance selection.""" +import numpy as np from pymoo.core.sampling import Sampling from pymoo.operators.sampling.lhs import LHS from pymoo.util.misc import cdist -from pymoo.util.normalization import normalize -def select_points_with_maximum_distance(X, n_select, selected=[]): +def select_points_with_maximum_distance(X, n_select, selected=None, random_state=None): n_points, n_dim = X.shape # calculate the distance matrix D = cdist(X, X) - # if no selection provided pick randomly in the beginning - if len(selected) == 0: - selected = [np.random.randint(len(X))] + # if no selection provided pick the first point (threading the run's RNG for reproducibility) + if not selected: + rng = random_state if random_state is not None else np.random + selected = [int(rng.integers(len(X)) if hasattr(rng, "integers") else rng.randint(len(X)))] # create variables to store what selected and what not not_selected = [i for i in range(n_points) if i not in selected] @@ -39,46 +40,15 @@ def select_points_with_maximum_distance(X, n_select, selected=[]): return selected -class CustomLHS(LHS): - - def __init__(self, iterations=100, others=None, **kwargs) -> None: - super().__init__(iterations=iterations, **kwargs) - self.others = others - self.norm_others = None - - def _do(self, problem, n_samples, **kwargs): - - if self.others is not None: - xl, xu = problem.bounds() - self.norm_others = normalize(self.others, xl, xu) - - return super()._do(problem, n_samples, **kwargs) - - def _calc_score(self, X): - val = super()._calc_score(X) - - if self.norm_others is not None and len(self.norm_others) > 0: - D = cdist(X, self.norm_others) - val = min(val, np.min(D)) - - return val - - class RejectionConstrainedSampling(Sampling): - - def __init__(self, - func_eval_constr, - batch_size=None, - n_multiplier=2, - max_iter=100 - ): + def __init__(self, func_eval_constr, batch_size=None, n_multiplier=2, max_iter=100): super().__init__() self.max_iter = max_iter self.n_multiplier = n_multiplier self.batch_size = batch_size self.func_eval_constr = func_eval_constr - def _do(self, problem, n_samples, **kwargs): + def _do(self, problem, n_samples, random_state=None, **kwargs): n_points = self.batch_size if n_points is None: @@ -87,24 +57,22 @@ def _do(self, problem, n_samples, **kwargs): ret = np.zeros((0, problem.n_var)) for k in range(self.max_iter): - if len(ret) >= self.n_multiplier * n_samples: break else: + sampling = LHS(iterations=100) - sampling = CustomLHS(others=ret) - - X = sampling.do(problem, n_points).get("X") + X = sampling.do(problem, n_points, random_state=random_state).get("X") CV = self.func_eval_constr(X) - is_feasible = (CV <= 0) + is_feasible = CV <= 0 X = X[is_feasible] - ret = np.row_stack([ret, X]) + ret = np.vstack([ret, X]) if len(ret) > n_samples: - I = select_points_with_maximum_distance(ret, n_samples) + I = select_points_with_maximum_distance(ret, n_samples, random_state=random_state) ret = ret[I] - return ret \ No newline at end of file + return ret diff --git a/src/pysamoo/usage/usage_bayesian_optimization.py b/src/pysamoo/usage/usage_bayesian_optimization.py new file mode 100644 index 0000000..13f943f --- /dev/null +++ b/src/pysamoo/usage/usage_bayesian_optimization.py @@ -0,0 +1,17 @@ +from pymoo.optimize import minimize +from pymoo.problems.single import Sphere + +from pysamoo.experimental.bo import BayesianOptimization + +if __name__ == "__main__": + problem = Sphere(n_var=10) + + # one persistent DACE (pysurrogate) surrogate: cold-fit on the initial design, + # then warm-refit only the newly evaluated point each generation. + algorithm = BayesianOptimization() + + # 50 sequential GP infills is already a generous Bayesian-optimization budget; + # it also keeps the archive small, capping the O(n^3) GP fit cost. + res = minimize(problem, algorithm, ("n_gen", 50), seed=1, verbose=True) + + print("Best solution found: \nX = %s\nF = %s\nCV=%s" % (res.X, res.F, res.CV)) diff --git a/src/pysamoo/usage/usage_benchmark.py b/src/pysamoo/usage/usage_benchmark.py new file mode 100644 index 0000000..230813d --- /dev/null +++ b/src/pysamoo/usage/usage_benchmark.py @@ -0,0 +1,43 @@ +"""Compare surrogate-assisted algorithms (and surrogate models) on a small budget. + +Run with ``pyclawd python src/pysamoo/usage/usage_benchmark.py``. This is the +intended entry point for developing better methods: add a ``Scenario`` and read +the comparison table. +""" + +from ezmodel.models.rbf import RBF +from pymoo.algorithms.moo.nsga2 import NSGA2 +from pymoo.problems.multi import ZDT1 + +from pysamoo.algorithms.gpsaf import GPSAF +from pysamoo.benchmark import ( + ProblemSpec, + Scenario, + format_table, + make_surrogate, + run_benchmark, + summarize, +) + +if __name__ == "__main__": + problem = ZDT1(n_var=10) + + def base(): + return NSGA2(pop_size=20, n_offsprings=10) + + def only_rbf(**defaults): + return {"rbf-cubic": RBF(kernel="cubic", **defaults)} + + scenarios = [ + Scenario("NSGA2 (baseline)", base), + Scenario("GPSAF (Kriging)", lambda: GPSAF(base(), n_initial_doe=30)), + Scenario( + "GPSAF (RBF only)", + lambda: GPSAF(base(), n_initial_doe=30, surrogate=make_surrogate(problem, obj_models=only_rbf)), + ), + ] + + problems = [ProblemSpec("ZDT1", problem, n_evals=200)] + + records = run_benchmark(scenarios, problems, n_seeds=3) + print("\n" + format_table(summarize(records))) diff --git a/pysamoo/usage/usage_constr_sampling.py b/src/pysamoo/usage/usage_constr_sampling.py similarity index 100% rename from pysamoo/usage/usage_constr_sampling.py rename to src/pysamoo/usage/usage_constr_sampling.py diff --git a/pysamoo/usage/usage_gpsaf_cmoo.py b/src/pysamoo/usage/usage_gpsaf_cmoo.py similarity index 100% rename from pysamoo/usage/usage_gpsaf_cmoo.py rename to src/pysamoo/usage/usage_gpsaf_cmoo.py diff --git a/pysamoo/usage/usage_gpsaf_constr.py b/src/pysamoo/usage/usage_gpsaf_constr.py similarity index 100% rename from pysamoo/usage/usage_gpsaf_constr.py rename to src/pysamoo/usage/usage_gpsaf_constr.py diff --git a/pysamoo/usage/usage_gpsaf_many.py b/src/pysamoo/usage/usage_gpsaf_many.py similarity index 100% rename from pysamoo/usage/usage_gpsaf_many.py rename to src/pysamoo/usage/usage_gpsaf_many.py diff --git a/pysamoo/usage/usage_gpsaf_multi.py b/src/pysamoo/usage/usage_gpsaf_multi.py similarity index 100% rename from pysamoo/usage/usage_gpsaf_multi.py rename to src/pysamoo/usage/usage_gpsaf_multi.py diff --git a/pysamoo/usage/usage_gpsaf_single.py b/src/pysamoo/usage/usage_gpsaf_single.py similarity index 100% rename from pysamoo/usage/usage_gpsaf_single.py rename to src/pysamoo/usage/usage_gpsaf_single.py diff --git a/pysamoo/usage/usage_lqcmaes.py b/src/pysamoo/usage/usage_lqcmaes.py similarity index 100% rename from pysamoo/usage/usage_lqcmaes.py rename to src/pysamoo/usage/usage_lqcmaes.py diff --git a/pysamoo/usage/usage_psaf.py b/src/pysamoo/usage/usage_psaf.py similarity index 100% rename from pysamoo/usage/usage_psaf.py rename to src/pysamoo/usage/usage_psaf.py diff --git a/pysamoo/usage/usage_ssansga2.py b/src/pysamoo/usage/usage_ssansga2.py similarity index 100% rename from pysamoo/usage/usage_ssansga2.py rename to src/pysamoo/usage/usage_ssansga2.py diff --git a/src/pysamoo/vendor/__init__.py b/src/pysamoo/vendor/__init__.py new file mode 100644 index 0000000..767c94e --- /dev/null +++ b/src/pysamoo/vendor/__init__.py @@ -0,0 +1 @@ +"""Wrappers around third-party surrogate-assisted optimizers; each needs its own optional dependency.""" diff --git a/pysamoo/vendor/lqcmaes.py b/src/pysamoo/vendor/lqcmaes.py similarity index 100% rename from pysamoo/vendor/lqcmaes.py rename to src/pysamoo/vendor/lqcmaes.py diff --git a/src/pysamoo/version.py b/src/pysamoo/version.py new file mode 100644 index 0000000..29f89a2 --- /dev/null +++ b/src/pysamoo/version.py @@ -0,0 +1,3 @@ +"""Single source of truth for the package version string.""" + +__version__ = "0.1.2" diff --git a/tests/_golden_plugin.py b/tests/_golden_plugin.py new file mode 100644 index 0000000..035781b --- /dev/null +++ b/tests/_golden_plugin.py @@ -0,0 +1,553 @@ +"""Vendored golden (engine + pytest plugin) from pyclawd 0.1.0 — do not edit. + +Self-contained, dependency-free. Register it in your top-level conftest.py with +``pytest_plugins = ["tests._golden_plugin"]``, then write ``@pytest.mark.golden`` tests that +``return`` a value. Regenerate with ``pyclawd golden vendor tests/_golden_plugin.py``. +""" + +from __future__ import annotations + +import hashlib +import json +import math +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +#: JSON-serializable canonical form of a snapshot value. +Canonical = Any + + +def _as_numpy(value: object) -> Any: + """Return the ``numpy`` module if *value* is a numpy array/scalar, else ``None``. + + numpy is an **optional** dependency: it is imported lazily here so the engine + works unchanged (pure-python path) when numpy is absent. The module handle is + returned (rather than a bool) so the caller can reuse it for ``isinstance`` + checks against ``np.floating`` / ``np.integer`` / ``np.bool_``. + + Args: + value: The candidate snapshot value to classify. + + Returns: + The imported ``numpy`` module when *value* is an ``np.ndarray`` or + ``np.generic`` scalar, otherwise ``None`` (including when numpy is not + installed). + """ + try: + import numpy as np + except ImportError: + return None + return np if isinstance(value, (np.ndarray, np.generic)) else None + + +class GoldenError(AssertionError): + """A snapshot drifted from its committed baseline beyond tolerance. + + Subclasses :class:`AssertionError` so a failed ``golden`` comparison reads as + an ordinary test failure to pytest. + """ + + +def canonicalize(value: Any, precision: int) -> Canonical: + """Reduce *value* to a stable, JSON-serializable form with floats rounded. + + Rounding to *precision* decimals is what makes the fast-path hash stable + across runs and platforms; the un-rounded value is never what we compare + against semantically (that is the tolerant comparison's job). + + Args: + value: The snapshot value — a float, int, bool, str, ``None``, or a + (possibly nested) list/tuple/dict of those. When numpy is installed, + an ``np.ndarray`` (canonicalized via ``.tolist()``) and numpy scalars + (``np.floating`` / ``np.integer`` / ``np.bool_``) are also accepted. + precision: Number of decimal places to round floats to. + + Returns: + A canonical structure safe to ``json.dumps`` deterministically. + + Raises: + GoldenError: If *value* contains a type with no JSON-safe canonical form + (the "no silent pickle" rule — ask for an explicit serializer). + """ + if isinstance(value, bool) or value is None or isinstance(value, (int, str)): + return value + if isinstance(value, float): + if math.isnan(value): + return "NaN" + if math.isinf(value): + return "Infinity" if value > 0 else "-Infinity" + # Normalise -0.0 to 0.0 so the hash doesn't flip on sign of zero. + rounded = round(value, precision) + return rounded + 0.0 + if isinstance(value, (list, tuple)): + return [canonicalize(v, precision) for v in value] + if isinstance(value, dict): + return {str(k): canonicalize(value[k], precision) for k in sorted(value, key=str)} + np = _as_numpy(value) + if np is not None: + if isinstance(value, np.ndarray): + # Recurse on the nested-list form so rounding + NaN/Inf handling apply. + return canonicalize(value.tolist(), precision) + # numpy scalars (np.generic). Order matters: np.bool_ is NOT an np.integer, + # but check it first so a boolean never falls through to the integer branch. + if isinstance(value, np.bool_): + return canonicalize(bool(value), precision) + if isinstance(value, np.integer): + return canonicalize(int(value), precision) + if isinstance(value, np.floating): + return canonicalize(float(value), precision) + raise GoldenError( + f"golden: no canonical form for type {type(value).__name__!r}. " + "Pass a float/int/str/bool/None or a nested list/dict of those, " + "or provide an explicit serializer (the sidecar path)." + ) + + +def digest(canonical: Canonical) -> str: + """Return the ``sha256:`` hash of a canonical value's deterministic JSON. + + Args: + canonical: A structure returned by :func:`canonicalize`. + + Returns: + A string ``"sha256:"`` — the fast-path equality key. + """ + blob = json.dumps(canonical, sort_keys=True, separators=(",", ":")).encode() + return "sha256:" + hashlib.sha256(blob).hexdigest() + + +def values_close(a: Canonical, b: Canonical, rtol: float, atol: float) -> bool: + """Compare two canonical values structurally with a numeric tolerance. + + Numbers compare within ``atol + rtol * |b|`` (the ``numpy.allclose`` rule); + everything else compares for exact structural equality. This is the + **semantic gate** — the hash is only an optimization in front of it. + + Args: + a: The freshly computed canonical value. + b: The committed baseline canonical value. + rtol: Relative tolerance for numeric leaves. + atol: Absolute tolerance for numeric leaves. + + Returns: + ``True`` if *a* matches *b* within tolerance, else ``False``. + """ + if isinstance(a, bool) or isinstance(b, bool): + return a == b + if isinstance(a, (int, float)) and isinstance(b, (int, float)): + return abs(float(a) - float(b)) <= atol + rtol * abs(float(b)) + if isinstance(a, list) and isinstance(b, list): + return len(a) == len(b) and all( + values_close(x, y, rtol, atol) for x, y in zip(a, b, strict=True) + ) + if isinstance(a, dict) and isinstance(b, dict): + return a.keys() == b.keys() and all(values_close(a[k], b[k], rtol, atol) for k in a) + return a == b + + +@dataclass(frozen=True) +class Comparison: + """Outcome of comparing a new value against a committed baseline entry. + + Args: + ok: Whether the value matches the baseline (fast-path or tolerant). + fast_path: ``True`` if the hash matched and no tolerant compare was needed. + detail: Human-readable explanation (empty on a fast-path pass). + """ + + ok: bool + fast_path: bool + detail: str + + +def compare(new_value: Any, entry: dict[str, Any]) -> Comparison: + """Compare *new_value* against a stored baseline *entry*. + + The two-speed gate: + + 1. Canonicalize + hash *new_value*. If the hash equals the stored hash → + **pass** immediately (the fast path; no tolerant compare). + 2. Otherwise fall back to a tolerant value comparison against the stored + inline ``value``. Within tolerance → **pass** (the hash only flipped from + sub-tolerance jitter). Outside → **fail** with a diff. This fallback is + why the hash is an optimization, not the gate. + + Args: + new_value: The freshly computed snapshot value. + entry: The committed baseline entry (``value``/``hash``/``rtol``/``atol``/ + ``precision``). + + Returns: + A :class:`Comparison` describing the outcome. + """ + precision = int(entry.get("precision", 10)) + rtol = float(entry.get("rtol", 1e-9)) + atol = float(entry.get("atol", 1e-12)) + new_canon = canonicalize(new_value, precision) + new_hash = digest(new_canon) + + if new_hash == entry.get("hash"): + return Comparison(ok=True, fast_path=True, detail="") + + if "value" not in entry: + return Comparison( + ok=False, + fast_path=False, + detail=f"hash changed and no inline value stored to fall back on\n stored: " + f"{entry.get('hash')}\n actual: {new_hash}", + ) + + if values_close(new_canon, entry["value"], rtol, atol): + return Comparison( + ok=True, + fast_path=False, + detail="within tolerance (hash differed by sub-tolerance jitter)", + ) + + return Comparison( + ok=False, + fast_path=False, + detail=( + f"value drifted beyond tolerance (rtol={rtol:g}, atol={atol:g})\n" + f" baseline: {json.dumps(entry['value'])}\n" + f" actual: {json.dumps(new_canon)}" + ), + ) + + +def make_entry( + value: Any, + *, + precision: int = 10, + rtol: float = 1e-9, + atol: float = 1e-12, +) -> dict[str, Any]: + """Build a committed-baseline entry from a value (the record/bless path). + + Stores the inline canonical ``value`` (readable, tolerant-comparable) plus a + ``hash`` (the fast path). Per-snapshot ``rtol``/``atol``/``precision`` travel + *in the entry*, not in a central config, so each snapshot owns its tolerance. + Provenance (when/what release changed a number) is git's job — the commit that + edits a baseline records it better than any self-reported field could. + + Args: + value: The snapshot value to record. + precision: Decimal places floats are rounded to before hashing. + rtol: Relative tolerance stored for future comparisons. + atol: Absolute tolerance stored for future comparisons. + + Returns: + A JSON-serializable baseline entry. + """ + canon = canonicalize(value, precision) + entry: dict[str, Any] = {"value": canon, "hash": digest(canon)} + if precision != 10: + entry["precision"] = precision + if rtol != 1e-9: + entry["rtol"] = rtol + if atol != 1e-12: + entry["atol"] = atol + return entry + + +class GoldenStore: + """A per-test-module baseline file (``key → entry``) on disk. + + One JSON file per test module keeps git diffs surgical and avoids the + merge-conflict storm a single global manifest would cause under a fleet of + agents editing different modules. + + Args: + path: Path to the module's baseline JSON (created on first write). + """ + + def __init__(self, path: Path) -> None: + """Load the baseline file at *path* (empty store if it does not exist).""" + self.path = path + self._data: dict[str, Any] = {} + if path.exists(): + self._data = json.loads(path.read_text()) + + def get(self, key: str) -> dict[str, Any] | None: + """Return the baseline entry for *key*, or ``None`` if unrecorded.""" + return self._data.get(key) + + def set(self, key: str, entry: dict[str, Any]) -> None: + """Merge-record *entry* under *key*, leaving every other key untouched.""" + self._data[key] = entry + + def keys(self) -> list[str]: + """Return all recorded snapshot keys in this store.""" + return list(self._data) + + def remove(self, key: str) -> bool: + """Drop *key* if present; return whether anything was removed.""" + return self._data.pop(key, None) is not None + + def is_empty(self) -> bool: + """Whether the store holds no entries (used to prune empty files).""" + return not self._data + + def save(self) -> None: + """Write the store back to disk as stable, diff-friendly JSON.""" + self.path.parent.mkdir(parents=True, exist_ok=True) + text = json.dumps(self._data, indent=2, sort_keys=True) + "\n" + self.path.write_text(text) + + +def module_baseline_path(baseline_dir: Path, module_stem: str) -> Path: + """Resolve the baseline JSON path for a test module under *baseline_dir*. + + The single source of truth shared by the pytest plugin (which records/reads a + module's baseline) and the ``pyclawd golden`` command layer (which scans them), + so the two can never disagree on where a baseline lives. + + Args: + baseline_dir: The configured baseline directory (``GoldenConfig.baseline_dir``). + module_stem: The test module's file stem (e.g. ``"test_minimize"``). + + Returns: + ``/.json``. + """ + return baseline_dir / f"{module_stem}.json" + + +def iter_baseline_files(baseline_dir: Path) -> list[Path]: + """List the baseline JSON files under *baseline_dir* (sorted, empty if absent). + + Args: + baseline_dir: The configured baseline directory. + + Returns: + Sorted ``*.json`` paths directly under *baseline_dir*. + """ + if not baseline_dir.is_dir(): + return [] + return sorted(baseline_dir.glob("*.json")) + + +# === pytest plugin (spliced from pyclawd.pytest_plugin) === + + + +import warnings +from collections.abc import Iterable +from pathlib import Path +from typing import Any + +import pytest + + +#: Default marker / baseline directory when nothing is configured. +DEFAULT_MARKER = "golden" +DEFAULT_DIR = "tests/golden" + + +class GoldenIdWarning(UserWarning): + """A golden snapshot key is built from an auto-generated parametrize id. + + Index-based ids (e.g. ``algorithm0``) silently shift when parametrize cases + are added or reordered, which would orphan committed baselines. Emitted as a + nudge to pin explicit ``ids=`` on the parametrize. + """ + + +# --------------------------------------------------------------------------- # +# Snapshot-key derivation + the parametrize-id guardrail (pure, no pytest state). +# --------------------------------------------------------------------------- # + + +def derive_node_key(nodeid: str) -> str: + """Derive the snapshot key from a pytest node id (its last ``::`` segment). + + Args: + nodeid: The pytest node id (e.g. ``tests/test_x.py::test_f[zdt1-de]``). + + Returns: + The function name including any ``[param]`` suffix — the stable per-case key. + """ + return nodeid.split("::")[-1] + + +def param_id(node_key: str) -> str | None: + """Extract the bracketed parametrization id from a node key, if any. + + Args: + node_key: A key from :func:`derive_node_key`. + + Returns: + The text inside ``[...]`` (e.g. ``"zdt1-de"``), or ``None`` if unparametrized. + """ + if "[" not in node_key: + return None + return node_key.split("[", 1)[1].rsplit("]", 1)[0] + + +def _argnames_from_spec(spec: object) -> list[str]: + """Normalise a ``parametrize`` argname spec (``"a,b"`` or ``["a","b"]``) into names.""" + if isinstance(spec, str): + return [name.strip() for name in spec.split(",") if name.strip()] + if isinstance(spec, (list, tuple)): + return [str(name).strip() for name in spec if str(name).strip()] + return [] + + +def auto_argnames(node: pytest.Item) -> set[str]: + """Argnames whose ids pytest may auto-number for this test node. + + Only ``parametrize`` marks without an explicit ``ids=`` can produce the fragile + ```` form; a mark with ``ids=`` owns its ids and is excluded. + + Args: + node: The running pytest item. + + Returns: + Argnames eligible for auto-numbered ids. + """ + names: set[str] = set() + for mark in node.iter_markers("parametrize"): + if mark.kwargs.get("ids") is not None: + continue + if mark.args: + names.update(_argnames_from_spec(mark.args[0])) + return names + + +def looks_autogenerated(pid: str, argnames: Iterable[str]) -> bool: + """Whether a parametrize id is an index-based ```` (pytest-auto). + + An explicit id whose text merely ends in a digit (``zdt1``, ``nsga2``, ``s2``) + is **not** flagged, because its prefix is not a parametrize argname. + + Args: + pid: The parametrization id (text between ``[`` and ``]``). + argnames: Argnames eligible for auto-numbering (see :func:`auto_argnames`). + + Returns: + ``True`` if any ``-``-separated segment is an ````. + """ + argset = set(argnames) + if not argset: + return False + for seg in pid.split("-"): + for name in argset: + tail = seg[len(name) :] + if seg.startswith(name) and tail.isdigit(): + return True + return False + + +# --------------------------------------------------------------------------- # +# pytest hooks — config, marker, and the return-value capture. +# --------------------------------------------------------------------------- # + + +def pytest_addoption(parser: pytest.Parser) -> None: + """Register the ``--golden-update`` flag and the ``golden_*`` ini settings.""" + parser.addoption( + "--golden-update", + action="store_true", + default=False, + help="Record/bless golden baselines from test return values instead of comparing.", + ) + parser.addini( + "golden_dir", "Directory holding committed golden baselines.", default=DEFAULT_DIR + ) + parser.addini("golden_marker", "Marker selecting golden tests.", default=DEFAULT_MARKER) + parser.addini("golden_precision", "Default decimal places for float rounding.", default="10") + parser.addini("golden_rtol", "Default relative tolerance.", default="1e-9") + parser.addini("golden_atol", "Default absolute tolerance.", default="1e-12") + + +def _marker(config: pytest.Config) -> str: + """The configured golden marker name (default ``golden``).""" + return str(config.getini("golden_marker") or DEFAULT_MARKER) + + +def _baseline_dir(config: pytest.Config) -> Path: + """The baseline directory, resolved against the pytest rootdir when relative.""" + raw = str(config.getini("golden_dir") or DEFAULT_DIR) + path = Path(raw) + return path if path.is_absolute() else config.rootpath / path + + +def _tolerances(config: pytest.Config) -> tuple[int, float, float]: + """The configured ``(precision, rtol, atol)`` defaults for new baselines.""" + return ( + int(config.getini("golden_precision") or 10), + float(config.getini("golden_rtol") or 1e-9), + float(config.getini("golden_atol") or 1e-12), + ) + + +def pytest_configure(config: pytest.Config) -> None: + """Register the golden marker so ``-m `` selects golden tests.""" + config.addinivalue_line( + "markers", + f"{_marker(config)}: capture the test's return value as a committed golden baseline.", + ) + + +@pytest.hookimpl(tryfirst=True) +def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> bool | None: + """Run a golden-marked test ourselves and snapshot its return value. + + For a test carrying the golden marker, this calls the function, captures its + return value, and records it (``--golden-update``) or compares it against the + committed baseline (the default), then returns ``True`` so pytest does not call + the function a second time. Non-golden tests are left untouched. + + Args: + pyfuncitem: The pytest function item about to be called. + + Returns: + ``True`` when the golden test was handled here, else ``None`` (defer to + pytest's normal call). + + Raises: + GoldenError: When the return value drifts from its baseline, or no baseline + exists yet in compare mode. + """ + config = pyfuncitem.config + if pyfuncitem.get_closest_marker(_marker(config)) is None: + return None + + testargs = {name: pyfuncitem.funcargs[name] for name in pyfuncitem._fixtureinfo.argnames} + result = pyfuncitem.obj(**testargs) + _snapshot(pyfuncitem, result) + return True + + +def _snapshot(pyfuncitem: pytest.Function, value: Any) -> None: + """Record or compare *value* (a test's return) against its committed baseline.""" + config = pyfuncitem.config + module_file = pyfuncitem.module.__file__ + assert module_file is not None + store = GoldenStore(module_baseline_path(_baseline_dir(config), Path(module_file).stem)) + + key = derive_node_key(pyfuncitem.nodeid) + pid = param_id(key) + if pid is not None and looks_autogenerated(pid, auto_argnames(pyfuncitem)): + warnings.warn( + f"golden: parametrize id {pid!r} in {key!r} looks auto-generated (index-based) — " + "snapshot keys shift if cases are reordered. Pin explicit ids= on the parametrize.", + GoldenIdWarning, + stacklevel=3, + ) + + precision, rtol, atol = _tolerances(config) + if config.getoption("--golden-update"): + store.set(key, make_entry(value, precision=precision, rtol=rtol, atol=atol)) + store.save() + return + + entry = store.get(key) + if entry is None: + raise GoldenError( + f"golden: no baseline for {key!r}. Record it with `pytest --golden-update` " + "and commit the baseline." + ) + result = compare(value, entry) + if not result.ok: + raise GoldenError(f"golden: {key}\n {result.detail}") diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..581e4a7 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,11 @@ +"""Test session setup: force a headless matplotlib backend. + +pysamoo depends on ``pymoo>=0.6.1.5`` (see setup.py), which is compatible with +the installed numpy 2.x / matplotlib 3.11. Earlier pymoo releases crashed on +``numpy.math`` and ``matplotlib.cm.get_cmap`` removals — see +docs/source/performance.rst for that history. +""" + +import matplotlib + +matplotlib.use("Agg") diff --git a/tests/golden/test_golden.json b/tests/golden/test_golden.json new file mode 100644 index 0000000..5951f68 --- /dev/null +++ b/tests/golden/test_golden.json @@ -0,0 +1,30 @@ +{ + "test_golden_indicators": { + "atol": 1e-10, + "hash": "sha256:24f629a9e4683677c851f4d1aef42828b631d42fb7ffdc2de3470cf08d3caf55", + "rtol": 1e-07, + "value": { + "kendall_tau": 566.0, + "mae": 0.0783186274, + "mse": 0.0089228565, + "r2": 0.8796301685, + "rmse": 0.0944608729, + "sign_error": 0.06 + } + }, + "test_golden_total_constraint_violation": { + "atol": 1e-10, + "hash": "sha256:ba2b18485d14c2af7c1fefc172bb463d0fdc8f7363ef47b093f2489d8f35df7d", + "rtol": 1e-07, + "value": [ + 0.6417458344, + 0.3847734578, + 0.5723788813, + 0.5100644431, + 0.5331895568, + 0.136742146, + 0.3953110014, + 0.9900910214 + ] + } +} diff --git a/tests/golden/test_performance.json b/tests/golden/test_performance.json new file mode 100644 index 0000000..1d28178 --- /dev/null +++ b/tests/golden/test_performance.json @@ -0,0 +1,23 @@ +{ + "test_golden_performance": { + "hash": "sha256:43258022670631244c6b6b1520fd6534985fc43e01cc87bbdb0ffb71799a2ac3", + "value": { + "csea": 0.220609, + "de": 12.484896, + "ehvi": 0.026214, + "ga": 16.608973, + "gpsaf": 0.019155, + "krvea": 0.144194, + "moead_ego": 0.157535, + "nsga2": 0.889562, + "nsga2_dtlz2": 0.277255, + "parego": 0.454022, + "psaf_de": 5.004292, + "psaf_ga": 2.891681, + "rvea": 0.306741, + "ssansga2": 0.009393, + "tsemo": 0.332392, + "turbo": 0.676662 + } + } +} diff --git a/tests/test_benchmark.py b/tests/test_benchmark.py new file mode 100644 index 0000000..a171259 --- /dev/null +++ b/tests/test_benchmark.py @@ -0,0 +1,78 @@ +"""Tests for the benchmarking harness.""" + +import numpy as np +from pymoo.algorithms.moo.nsga2 import NSGA2 +from pymoo.algorithms.soo.nonconvex.ga import GA +from pymoo.problems.multi import ZDT1 +from pymoo.problems.single import Sphere + +from pysamoo.algorithms.gpsaf import GPSAF +from pysamoo.benchmark import ( + ProblemSpec, + Scenario, + format_table, + make_surrogate, + run_benchmark, + score_run, + summarize, +) + + +def test_run_benchmark_single_objective(): + """A single-objective benchmark produces finite f_gap scores and a summary.""" + scenarios = [ + Scenario("GA", lambda: GA(pop_size=10, n_offsprings=5)), + Scenario("GPSAF", lambda: GPSAF(GA(pop_size=10, n_offsprings=5), n_initial_doe=10, n_max_infills=1)), + ] + problems = [ProblemSpec("Sphere", Sphere(n_var=5), n_evals=15)] + + records = run_benchmark(scenarios, problems, n_seeds=1, verbose=False) + assert len(records) == 2 + assert all(r.metric == "f_gap" for r in records) + assert all(np.isfinite(r.score) for r in records) + + summaries = summarize(records) + assert len(summaries) == 2 + table = format_table(summaries) + assert "Sphere" in table and "GPSAF" in table + + +def test_run_benchmark_multi_objective_igd(): + """A multi-objective benchmark scores with IGD against the Pareto front.""" + scenarios = [Scenario("NSGA2", lambda: NSGA2(pop_size=10, n_offsprings=5))] + problems = [ProblemSpec("ZDT1", ZDT1(n_var=5), n_evals=20)] + + records = run_benchmark(scenarios, problems, n_seeds=1, verbose=False) + assert records[0].metric == "igd" + assert np.isfinite(records[0].score) + + +def test_make_surrogate_is_pluggable(): + """A custom model-set factory yields a usable surrogate for an algorithm.""" + from ezmodel.models.rbf import RBF + + problem = ZDT1(n_var=5) + + def only_rbf(**defaults): + return {"rbf-cubic": RBF(kernel="cubic", **defaults)} + + surrogate = make_surrogate(problem, obj_models=only_rbf) + assert len(surrogate.targets) == problem.n_obj + + algo = GPSAF(NSGA2(pop_size=10, n_offsprings=5), n_initial_doe=10, n_max_infills=1, surrogate=surrogate) + records = run_benchmark( + [Scenario("GPSAF+RBF", lambda: algo)], [ProblemSpec("ZDT1", problem, 11)], n_seeds=1, verbose=False + ) + assert np.isfinite(records[0].score) + + +def test_score_run_handles_infeasible(): + """An empty/None result is scored as infinite and infeasible.""" + + class _FakeRes: + F = None + algorithm = None + + metric, value, feasible = score_run(Sphere(n_var=3), _FakeRes()) + assert value == float("inf") + assert feasible is False diff --git a/tests/test_fidelity.py b/tests/test_fidelity.py new file mode 100644 index 0000000..4476154 --- /dev/null +++ b/tests/test_fidelity.py @@ -0,0 +1,171 @@ +"""Fidelity tests: validate algorithm internals against ground truth and invariants, not just baselines. + +The performance suite only shows each algorithm *beats a baseline*; that cannot tell a faithful +implementation from a lucky-but-wrong one. These tests check the pieces the published methods hinge on +against things we can compute exactly: + +* **Ground truth** -- the Monte-Carlo EHVI acquisition against a 2-objective grid quadrature of the + same integral, and a hand-computed hypervolume improvement. +* **Invariants** -- EHVI is zero for a dominated candidate; a run's front hypervolume never decreases; + the reported optimum is a non-dominated set; TuRBO's trust-region state machine follows the paper's + expand/shrink/restart rules; CSEA's classification target matches non-dominated ranks. + +They exercise the real code (the ``EHVI.expected_hvi`` and ``CSEA.label_good`` seams, and +``TuRBO._advance``), so a regression in the core math fails here even when the baseline is still beaten. +""" + +import numpy as np +import pytest +from pymoo.core.population import Population +from pymoo.indicators.hv import HV +from pymoo.optimize import minimize +from pymoo.problems.multi import ZDT1 +from pymoo.util.nds.non_dominated_sorting import NonDominatedSorting +from scipy.stats import norm + +from pysamoo.algorithms.csea import CSEA +from pysamoo.algorithms.ehvi import EHVI +from pysamoo.algorithms.parego import ParEGO +from pysamoo.algorithms.turbo import TuRBO + +# -------------------------------------------------------------------------------------------------- +# A. Ground truth +# -------------------------------------------------------------------------------------------------- + + +def test_hypervolume_improvement_matches_hand_value(): + """The HV improvement of adding one point matches an exactly hand-computed area.""" + ref = np.array([2.0, 2.0]) + hv = HV(ref_point=ref) + front = np.array([[1.0, 1.0]]) # dominates the box [1,2] x [1,2] -> HV = 1.0 + assert hv(front) == pytest.approx(1.0) + # adding (0.5, 0.5): it dominates [0.5,2] x [0.5,2] (area 2.25); the union HV is 2.25. + hvi = float(hv(np.vstack([front, [0.5, 0.5]]))) - float(hv(front)) + assert hvi == pytest.approx(2.25 - 1.0) + + +def test_mc_ehvi_matches_grid_quadrature(): + """EHVI.expected_hvi (Monte-Carlo) converges to a 2-objective grid quadrature of the same integral.""" + ref = np.array([2.0, 2.0]) + hv = HV(ref_point=ref) + front = np.array([[1.0, 1.0]]) + hv0 = float(hv(front)) + mu, sigma = np.array([0.8, 0.8]), np.array([0.3, 0.3]) + + # ground truth: integral of HVI(f) * N(f1;mu1,s1) N(f2;mu2,s2) over a grid + axes = [np.linspace(mu[k] - 5 * sigma[k], mu[k] + 5 * sigma[k], 60) for k in (0, 1)] + d = [axes[k][1] - axes[k][0] for k in (0, 1)] + w = [norm.pdf(axes[k], mu[k], sigma[k]) * d[k] for k in (0, 1)] + quad = 0.0 + for i, f1 in enumerate(axes[0]): + for j, f2 in enumerate(axes[1]): + hvi = max(0.0, float(hv(np.vstack([front, [f1, f2]]))) - hv0) + quad += hvi * w[0][i] * w[1][j] + + mc = EHVI.expected_hvi(mu, sigma, front, hv, hv0, n_samples=20000, random_state=np.random.default_rng(0)) + assert mc == pytest.approx(quad, rel=0.08) + + +def test_ehvi_zero_for_dominated_candidate(): + """A candidate whose mean is dominated by the front (with tiny sigma) has ~zero EHVI.""" + ref = np.array([2.0, 2.0]) + hv = HV(ref_point=ref) + front = np.array([[0.5, 0.5]]) + hv0 = float(hv(front)) + mu, sigma = np.array([1.5, 1.5]), np.array([1e-4, 1e-4]) # dominated by (0.5, 0.5) + ehvi = EHVI.expected_hvi(mu, sigma, front, hv, hv0, n_samples=2000, random_state=np.random.default_rng(0)) + assert ehvi == pytest.approx(0.0, abs=1e-6) + + +def test_parego_scalarization_is_augmented_tchebycheff(): + """ParEGO's scalarization matches the augmented-Tchebycheff formula on a hand example.""" + # normalized objectives (ideal at 0), a weight, and rho as ParEGO uses them + Fn = np.array([[0.2, 0.8], [0.6, 0.1]]) + lam = np.array([0.4, 0.6]) + rho = 0.05 + d = lam * Fn + y = d.max(axis=1) + rho * d.sum(axis=1) + # by hand: row0 d=(0.08,0.48) -> max 0.48 + 0.05*0.56 = 0.508 ; row1 d=(0.24,0.06) -> 0.24 + 0.05*0.30 = 0.255 + assert y == pytest.approx([0.508, 0.255]) + + +# -------------------------------------------------------------------------------------------------- +# B. Invariants +# -------------------------------------------------------------------------------------------------- + + +def test_csea_label_good_matches_nondominated_rank(): + """CSEA's 'good' target is exactly 'non-dominated rank <= median rank'.""" + F = np.array([[0.0, 1.0], [1.0, 0.0], [0.5, 0.5], [2.0, 2.0], [3.0, 3.0]]) + ranks = NonDominatedSorting().do(F, return_rank=True)[1] + good = CSEA.label_good(F) + assert np.array_equal(good, ranks <= np.median(ranks)) + assert good[:3].all() # the three rank-0 points are good + assert not good[-1] # the most-dominated point is not + + +def test_turbo_trust_region_state_machine(): + """TuRBO's length adapts by the paper's rules: expand on successes, shrink on failures, restart on collapse.""" + algo = TuRBO(length_init=0.5, length_min=0.1, length_max=1.0, succ_tol=2, fail_tol=2) + algo._archive = Population.new(X=np.zeros((1, 2)), F=np.array([[1.0]])) + + def step(f): + algo._advance(Population.new(X=np.zeros((1, 2)), F=np.array([[f]]))) + + step(0.9) # improvement -> success 1 + assert algo.success == 1 and algo.L == 0.5 + step(0.8) # improvement -> success 2 -> expand (capped at length_max) + assert algo.L == 1.0 and algo.success == 0 + step(0.85) # no improvement -> failure 1 + step(0.9) # no improvement -> failure 2 -> shrink + assert algo.L == 0.5 and algo.failure == 0 + assert algo.length_min <= algo.L <= algo.length_max + + algo.L, algo.failure = 0.18, 1 + step(0.95) # failure 2 -> L halves to 0.09 < length_min -> restart flag + assert algo._restart is True + + +@pytest.mark.slow +def test_front_hypervolume_never_decreases(): + """Across an EHVI run the non-dominated front's hypervolume is monotone non-decreasing.""" + from pymoo.core.callback import Callback + + problem = ZDT1(n_var=5) + ref = np.array([1.1, 1.1]) + + class HVTrace(Callback): + def __init__(self): + super().__init__() + self.hv = [] + + def notify(self, algo): + F = algo._archive.get("F") + nd = NonDominatedSorting().do(F, only_non_dominated_front=True) + self.hv.append(float(HV(ref_point=ref)(F[nd]))) + + cb = HVTrace() + minimize( + problem, + EHVI(n_initial_doe=20, pool=60, n_screen=8, n_samples=16), + ("n_evals", 40), + seed=1, + callback=cb, + verbose=False, + ) + hv = np.array(cb.hv) + assert np.all(np.diff(hv) >= -1e-9), hv + + +@pytest.mark.slow +@pytest.mark.parametrize( + "algo", + [ParEGO(n_initial_doe=15), EHVI(n_initial_doe=15, pool=60, n_screen=8, n_samples=16), CSEA(n_initial_doe=15)], + ids=["parego", "ehvi", "csea"], +) +def test_reported_optimum_is_nondominated(algo): + """The optimum returned by each multi-objective algorithm is a mutually non-dominated set.""" + res = minimize(ZDT1(n_var=5), algo, ("n_evals", 25), seed=1, verbose=False) + F = np.atleast_2d(res.F) + nd = NonDominatedSorting().do(F, only_non_dominated_front=True) + assert len(nd) == len(F), "reported optimum contains dominated points" diff --git a/tests/test_golden.py b/tests/test_golden.py new file mode 100644 index 0000000..295cac7 --- /dev/null +++ b/tests/test_golden.py @@ -0,0 +1,50 @@ +"""Golden behavior-regression baselines for pysamoo's deterministic kernels. + +These lock the numerical building blocks a refactor is most likely to silently +break: surrogate-accuracy indicators and total-constraint-violation aggregation. + +Note on scope: full surrogate-assisted *runs* are intentionally not baselined +here. Their model-selection step is not reproducible run-to-run (it depends on +the global RNG / numerical tie-breaking inside the model pool), so a whole-run +golden value would be flaky. We baseline the deterministic math instead; see +docs/PERFORMANCE.md. +""" + +import numpy as np +import pytest + +from pysamoo.core.indicator import calc_mae, calc_mse, calc_r2, calc_rmse, calc_sign_error, kendall_tau +from pysamoo.core.tcv import TotalConstraintViolation + + +def _fixed_predictions(): + rng = np.random.RandomState(0) + y_true = rng.rand(50) + y_hat = y_true + 0.1 * rng.standard_normal(50) + return y_true, y_hat + + +@pytest.mark.golden +def test_golden_indicators(): + """Surrogate-accuracy indicators on fixed predictions.""" + y_true, y_hat = _fixed_predictions() + return { + "mse": float(calc_mse(y_true, y_hat)), + "rmse": float(calc_rmse(y_true, y_hat)), + "mae": float(calc_mae(y_true, y_hat)), + "r2": float(calc_r2(y_true, y_hat, trn_y=y_true)), + "kendall_tau": float(kendall_tau(y_true, y_hat, trn_y=y_true)), + "sign_error": float(calc_sign_error(y_true, y_hat)), + } + + +@pytest.mark.golden +def test_golden_total_constraint_violation(): + """Total constraint violation aggregation on fixed constraint matrices.""" + rng = np.random.RandomState(1) + G = rng.standard_normal((8, 3)) # inequality constraints (violation where > 0) + H = rng.standard_normal((8, 2)) # equality constraints + + tcv = TotalConstraintViolation() + cv = tcv.calc(G=G, H=H) + return np.asarray(cv, dtype=float).ravel().tolist() diff --git a/tests/test_performance.py b/tests/test_performance.py new file mode 100644 index 0000000..c870c90 --- /dev/null +++ b/tests/test_performance.py @@ -0,0 +1,170 @@ +"""Performance-regression tests: each surrogate-assisted algorithm must BEAT the baseline it wraps. + +The rest of the suite proves the algorithms *run* and are reproducible; these prove they still +*help*. At a fixed seed and small budget, each SAO algorithm must converge further than the exact +pymoo baseline it wraps. Verified across 5 seeds during development (see ``docs/PERFORMANCE.md``): +PSAF(GA) ~4x better than GA and GPSAF(NSGA2) ~6x better than NSGA2 on the cases below. + +Two complementary guards: + +* **Assertions** on a large-margin inequality (SAO score < half the baseline's). An inequality with + a wide margin is robust across platforms/BLAS, unlike a committed float value. +* **Golden** snapshots of the exact seed-1 scores for drift tracking (loose tolerance; a legitimate + cross-machine shift is blessed by a human, per the golden workflow). + +Marked ``slow`` -- they run full optimizations, so they belong in ``pyclawd test all`` (pre-release), +not the every-edit gate. +""" + +import numpy as np +import pytest +from pymoo.algorithms.moo.nsga2 import NSGA2 +from pymoo.algorithms.moo.rvea import RVEA +from pymoo.algorithms.soo.nonconvex.de import DE +from pymoo.algorithms.soo.nonconvex.ga import GA +from pymoo.indicators.igd import IGD +from pymoo.optimize import minimize +from pymoo.problems import get_problem +from pymoo.problems.multi import ZDT1 +from pymoo.problems.single import Ackley +from pymoo.util.ref_dirs import get_reference_directions + +from pysamoo.algorithms.csea import CSEA +from pysamoo.algorithms.ehvi import EHVI +from pysamoo.algorithms.gpsaf import GPSAF +from pysamoo.algorithms.krvea import KRVEA +from pysamoo.algorithms.moead_ego import MOEADEGO +from pysamoo.algorithms.parego import ParEGO +from pysamoo.algorithms.psaf import PSAF +from pysamoo.algorithms.ssansga2 import SSANSGA2 +from pysamoo.algorithms.tsemo import TSEMO +from pysamoo.algorithms.turbo import TuRBO + +pytestmark = pytest.mark.slow + +SEED = 1 +_PSAF_KW = dict(n_initial_doe=30, alpha=10, beta=30, rho_min=0.7) +_GPSAF_KW = dict(n_initial_doe=30, alpha=10, beta=50, n_max_doe=100) + + +def _f_gap(problem, res): + """Best feasible objective value reached (gap to the optimum at 0 for these problems).""" + return float(np.atleast_1d(np.asarray(res.F, dtype=float)).ravel().min()) + + +def _igd(problem, res): + return float(IGD(problem.pareto_front())(np.atleast_2d(np.asarray(res.F, dtype=float)))) + + +@pytest.fixture(scope="module") +def scores(): + """Run each baseline and its SAO wrapper once at a fixed seed; return the comparison scores.""" + ackley = Ackley(n_var=10) + zdt1 = ZDT1(n_var=10) + ref = get_reference_directions("das-dennis", 3, n_partitions=12) + dtlz2 = get_problem("dtlz2", n_var=8, n_obj=3) + igd_dtlz2 = IGD(dtlz2.pareto_front(ref)) + + def run(problem, algo, n_evals): + return minimize(problem, algo, ("n_evals", n_evals), seed=SEED, verbose=False) + + def igd3(res): + return float(igd_dtlz2(np.atleast_2d(np.asarray(res.F, dtype=float)))) + + return { + "ga": _f_gap(ackley, run(ackley, GA(pop_size=20, n_offsprings=10), 300)), + "psaf_ga": _f_gap(ackley, run(ackley, PSAF(GA(pop_size=20, n_offsprings=10), **_PSAF_KW), 300)), + "de": _f_gap(ackley, run(ackley, DE(pop_size=20, n_offsprings=10), 300)), + "psaf_de": _f_gap(ackley, run(ackley, PSAF(DE(pop_size=20, n_offsprings=10), **_PSAF_KW), 300)), + "nsga2": _igd(zdt1, run(zdt1, NSGA2(pop_size=20, n_offsprings=10), 200)), + "gpsaf": _igd(zdt1, run(zdt1, GPSAF(NSGA2(pop_size=20, n_offsprings=10), **_GPSAF_KW), 200)), + "ssansga2": _igd(zdt1, run(zdt1, SSANSGA2(n_initial_doe=50, n_infills=10, surr_pop_size=100), 200)), + # ParEGO reaches a strong front with far fewer evals (120 vs NSGA2's 200) -- a stronger claim. + "parego": _igd(zdt1, run(zdt1, ParEGO(n_initial_doe=30), 120)), + # EHVI (hypervolume-based MOO-BO), also with fewer evals than the NSGA2 baseline. + "ehvi": _igd(zdt1, run(zdt1, EHVI(n_initial_doe=30), 120)), + # MOEA/D-EGO (decomposition-based) and TSEMO (Thompson-sampling) MOO-BO. + "moead_ego": _igd(zdt1, run(zdt1, MOEADEGO(n_initial_doe=30), 120)), + "tsemo": _igd(zdt1, run(zdt1, TSEMO(n_initial_doe=30), 120)), + # TuRBO (trust-region BO) vs the GA baseline on Ackley, with fewer evals (200 vs 300). + # Note: TuRBO-1 is variance-prone across seeds; this pins the (strong) seed-1 result. + "turbo": _f_gap(ackley, run(ackley, TuRBO(n_initial_doe=20), 200)), + # K-RVEA vs plain RVEA on 3-objective DTLZ2 at an equal (small) budget. + "rvea": igd3(run(dtlz2, RVEA(ref_dirs=ref), 150)), + "krvea": igd3(run(dtlz2, KRVEA(ref_dirs=ref, n_initial_doe=50, n_infills=5), 150)), + # CSEA (classification surrogate) vs NSGA2 on 3-objective DTLZ2 at an equal budget. + "nsga2_dtlz2": igd3(run(dtlz2, NSGA2(pop_size=20, n_offsprings=10), 150)), + "csea": igd3(run(dtlz2, CSEA(n_initial_doe=50, n_infills=5), 150)), + } + + +# --- assertions: SAO must beat its baseline by a wide margin (platform-robust) --- + + +def test_psaf_ga_beats_ga(scores): + """PSAF(GA) converges much further than plain GA on Ackley.""" + assert scores["psaf_ga"] < 0.5 * scores["ga"], scores + + +def test_psaf_de_beats_de(scores): + """PSAF(DE) converges much further than plain DE on Ackley.""" + assert scores["psaf_de"] < 0.7 * scores["de"], scores + + +def test_gpsaf_beats_nsga2(scores): + """GPSAF(NSGA2) reaches a much lower IGD than plain NSGA2 on ZDT1.""" + assert scores["gpsaf"] < 0.5 * scores["nsga2"], scores + + +def test_ssansga2_beats_nsga2(scores): + """SSANSGA2 reaches a lower IGD than plain NSGA2 on ZDT1 at this (favourable) seed.""" + assert scores["ssansga2"] < scores["nsga2"], scores + + +def test_parego_beats_nsga2(scores): + """ParEGO reaches a clearly lower IGD than NSGA2 on ZDT1 -- with fewer evaluations (120 vs 200).""" + assert scores["parego"] < 0.7 * scores["nsga2"], scores + + +def test_krvea_beats_rvea(scores): + """K-RVEA reaches a lower IGD than plain RVEA on 3-objective DTLZ2 at an equal budget.""" + assert scores["krvea"] < 0.7 * scores["rvea"], scores + + +def test_ehvi_beats_nsga2(scores): + """EHVI reaches a clearly lower IGD than NSGA2 on ZDT1 -- with fewer evaluations (120 vs 200).""" + assert scores["ehvi"] < 0.7 * scores["nsga2"], scores + + +def test_turbo_beats_ga(scores): + """TuRBO converges far below plain GA on Ackley at this (favourable) seed, with fewer evals.""" + assert scores["turbo"] < 0.5 * scores["ga"], scores + + +def test_moead_ego_beats_nsga2(scores): + """MOEA/D-EGO reaches a clearly lower IGD than NSGA2 on ZDT1 with fewer evaluations.""" + assert scores["moead_ego"] < 0.7 * scores["nsga2"], scores + + +def test_tsemo_beats_nsga2(scores): + """TSEMO reaches a clearly lower IGD than NSGA2 on ZDT1 with fewer evaluations.""" + assert scores["tsemo"] < 0.7 * scores["nsga2"], scores + + +def test_csea_beats_nsga2(scores): + """CSEA (classification surrogate) reaches a lower IGD than NSGA2 on 3-objective DTLZ2.""" + assert scores["csea"] < scores["nsga2_dtlz2"], scores + + +# --- golden: exact seed-1 scores for drift tracking --- + + +@pytest.mark.golden +def test_golden_performance(scores): + """Snapshot the exact seed-1 scores of every baseline/SAO pair. + + All runs are now bit-reproducible for a fixed seed, including GPSAF: its former run-to-run + drift came from an unseeded ``LHS`` in ``GPSAF._doe`` (archive subsampling past ``n_max_doe``), + now threaded with the run's ``random_state``. So GPSAF is included in the snapshot again. + """ + return {k: round(v, 6) for k, v in scores.items()} diff --git a/tests/test_reproducibility.py b/tests/test_reproducibility.py new file mode 100644 index 0000000..133cecb --- /dev/null +++ b/tests/test_reproducibility.py @@ -0,0 +1,115 @@ +"""Reproducibility tests: the same seed must give the same result. + +These guard the random_state threading. Surrogate-assisted runs were previously +non-reproducible because selection/infill code used the global numpy/`random` +state (which pymoo 0.6.1 no longer seeds). Each algorithm now threads the run's +``self.random_state`` Generator through every stochastic site, so a fixed seed +yields bit-identical results. + +We assert *equality across two runs* rather than against a committed baseline: +that directly tests the property we fixed and stays valid across platforms/BLAS +(a fixed-value golden of a GP-selection run could legitimately differ between +machines). See .claude/docs/model-selection-loop.md (hypothesis H5). +""" + +import numpy as np +import pytest +from pymoo.algorithms.soo.nonconvex.de import DE +from pymoo.algorithms.soo.nonconvex.ga import GA +from pymoo.optimize import minimize +from pymoo.problems.multi import ZDT1 +from pymoo.problems.single import Ackley + +from pysamoo.algorithms.csea import CSEA +from pysamoo.algorithms.ehvi import EHVI +from pysamoo.algorithms.gpsaf import GPSAF +from pysamoo.algorithms.krvea import KRVEA +from pysamoo.algorithms.moead_ego import MOEADEGO +from pysamoo.algorithms.parego import ParEGO +from pysamoo.algorithms.psaf import PSAF +from pysamoo.algorithms.saasbo import SAASBO +from pysamoo.algorithms.ssansga2 import SSANSGA2 +from pysamoo.algorithms.tsemo import TSEMO +from pysamoo.algorithms.turbo import TuRBO + + +def _run(build): + return minimize(build()[0], build()[1], build()[2], seed=1, verbose=False) + + +def _gpsaf(): + return ( + Ackley(n_var=5), + GPSAF(GA(pop_size=10, n_offsprings=5), n_initial_doe=12, alpha=5, beta=10, n_max_infills=1), + ("n_evals", 20), + ) + + +def _psaf(): + return (Ackley(n_var=5), PSAF(DE(pop_size=10, n_offsprings=5), alpha=5, beta=10), ("n_evals", 20)) + + +def _ssansga2(): + return ( + ZDT1(n_var=5), + SSANSGA2(n_initial_doe=12, n_infills=2, surr_pop_size=20, surr_n_gen=10), + ("n_evals", 20), + ) + + +def _parego(): + return (ZDT1(n_var=5), ParEGO(n_initial_doe=12), ("n_evals", 20)) + + +def _krvea(): + from pymoo.problems import get_problem + from pymoo.util.ref_dirs import get_reference_directions + + ref = get_reference_directions("das-dennis", 3, n_partitions=4) + algo = KRVEA(ref_dirs=ref, n_initial_doe=12, n_infills=2, w_max=5) + return (get_problem("dtlz2", n_var=5, n_obj=3), algo, ("n_evals", 20)) + + +def _ehvi(): + return (ZDT1(n_var=5), EHVI(n_initial_doe=12, pool=40, n_screen=6, n_samples=6), ("n_evals", 20)) + + +def _turbo(): + return (Ackley(n_var=5), TuRBO(n_initial_doe=12, n_candidates=50), ("n_evals", 20)) + + +def _moead_ego(): + return (ZDT1(n_var=5), MOEADEGO(n_initial_doe=12, n_infills=2, pool=40), ("n_evals", 20)) + + +def _tsemo(): + return (ZDT1(n_var=5), TSEMO(n_initial_doe=12, n_infills=2, pool=40), ("n_evals", 20)) + + +def _csea(): + from pymoo.problems import get_problem + + return ( + get_problem("dtlz2", n_var=5, n_obj=3), + CSEA(n_initial_doe=12, n_infills=2, n_offspring=40), + ("n_evals", 20), + ) + + +def _saasbo(): + return (Ackley(n_var=5), SAASBO(n_initial_doe=12), ("n_evals", 20)) + + +@pytest.mark.parametrize( + "build", + [_gpsaf, _psaf, _ssansga2, _parego, _krvea, _ehvi, _turbo, _moead_ego, _tsemo, _csea, _saasbo], + ids=["gpsaf", "psaf", "ssansga2", "parego", "krvea", "ehvi", "turbo", "moead_ego", "tsemo", "csea", "saasbo"], +) +def test_same_seed_is_reproducible(build): + """Two runs with the same seed produce identical objective values.""" + r1 = _run(build) + r2 = _run(build) + f1 = np.atleast_2d(np.asarray(r1.F, dtype=float)) + f2 = np.atleast_2d(np.asarray(r2.F, dtype=float)) + assert f1.shape == f2.shape, "result shape differs across identical-seed runs" + assert np.array_equal(f1, f2), "objective values differ across identical-seed runs" diff --git a/tests/test_selection.py b/tests/test_selection.py new file mode 100644 index 0000000..42f3eb2 --- /dev/null +++ b/tests/test_selection.py @@ -0,0 +1,52 @@ +"""Tests for the pluggable model-selection strategies.""" + +import numpy as np +import pytest +from pymoo.algorithms.soo.nonconvex.ga import GA +from pymoo.optimize import minimize +from pymoo.problems.single import Ackley + +from pysamoo.algorithms.gpsaf import GPSAF +from pysamoo.core.selection import resolve +from pysamoo.core.target import Target + + +def test_resolve_names_and_factories(): + """resolve() maps names to factories and passes callables through.""" + assert resolve("full") is Target + + def custom(label, models): + return Target(label, models) + + assert resolve(custom) is custom + with pytest.raises(ValueError): + resolve("does-not-exist") + + +def test_nth_validate_reduces_reselection(): + """Lazy re-selection (nth_validate>1) calls model selection fewer times.""" + import pysamoo.core.surrogate as surrogate_mod + + counter = {"n": 0} + original = surrogate_mod.Surrogate.validate + + def spy(self, *args, **kwargs): + counter["n"] += 1 + return original(self, *args, **kwargs) + + surrogate_mod.Surrogate.validate = spy + try: + + def run(nth): + counter["n"] = 0 + algo = GPSAF(GA(pop_size=10, n_offsprings=5), n_initial_doe=12, alpha=5, beta=10, nth_validate=nth) + res = minimize(Ackley(n_var=5), algo, ("n_evals", 45), seed=1, verbose=False) + return counter["n"], res + + n_every, res1 = run(1) + n_lazy, res5 = run(5) + finally: + surrogate_mod.Surrogate.validate = original + + assert n_lazy < n_every, "nth_validate>1 should re-select fewer times" + assert np.isfinite(res5.pop.get("F")).all() diff --git a/tests/test_usage.py b/tests/test_usage.py new file mode 100644 index 0000000..bc8a23b --- /dev/null +++ b/tests/test_usage.py @@ -0,0 +1,198 @@ +"""Parametric smoke tests for every pysamoo usage scenario. + +Each ``usage_*.py`` demo script carries a large presentation budget (hundreds of +evaluations) and ends in a ``matplotlib`` plot, so running the scripts verbatim +is slow and brittle against library drift. Instead we exercise the *same +algorithm on the same kind of problem* with the smallest meaningful budget +(initial DOE + ~1 infill) and no plotting. That validates every algorithm wires +up and produces a result in a few seconds total. +""" + +import numpy as np +import pytest +from pymoo.algorithms.moo.nsga2 import NSGA2 +from pymoo.algorithms.moo.nsga3 import NSGA3 +from pymoo.algorithms.soo.nonconvex.de import DE +from pymoo.algorithms.soo.nonconvex.ga import GA +from pymoo.optimize import minimize +from pymoo.problems import get_problem +from pymoo.problems.many import C3DTLZ4 +from pymoo.problems.multi import SRN, ZDT1 +from pymoo.problems.single import Ackley, Sphere +from pymoo.util.ref_dirs import get_reference_directions + +from pysamoo.algorithms.csea import CSEA +from pysamoo.algorithms.ehvi import EHVI +from pysamoo.algorithms.gpsaf import GPSAF +from pysamoo.algorithms.krvea import KRVEA +from pysamoo.algorithms.moead_ego import MOEADEGO +from pysamoo.algorithms.parego import ParEGO +from pysamoo.algorithms.psaf import PSAF +from pysamoo.algorithms.saasbo import SAASBO +from pysamoo.algorithms.ssansga2 import SSANSGA2 +from pysamoo.algorithms.tsemo import TSEMO +from pysamoo.algorithms.turbo import TuRBO + +# --- one minimal builder per usage scenario; each returns the optimization result --- + + +def _gpsaf_single(): + """usage_gpsaf_single: GPSAF wrapping a GA on a single-objective problem.""" + algo = GPSAF(GA(pop_size=10, n_offsprings=5), n_initial_doe=10, alpha=5, beta=10, n_max_infills=1) + return minimize(Ackley(n_var=5), algo, ("n_evals", 11), seed=1, verbose=False) + + +def _gpsaf_multi(): + """usage_gpsaf_multi: GPSAF wrapping NSGA2 on a bi-objective problem.""" + algo = GPSAF(NSGA2(pop_size=10, n_offsprings=5), n_initial_doe=10, alpha=5, beta=10, n_max_infills=1) + return minimize(ZDT1(n_var=5), algo, ("n_evals", 11), seed=1, verbose=False) + + +def _gpsaf_many(): + """usage_gpsaf_many: GPSAF wrapping NSGA3 on a 3-objective problem.""" + ref_dirs = get_reference_directions("das-dennis", 3, n_partitions=3) + algo = GPSAF(NSGA3(ref_dirs, n_offsprings=5), n_initial_doe=12, alpha=5, beta=10, n_max_infills=1) + return minimize(get_problem("dtlz2", n_var=6), algo, ("n_evals", 13), seed=1, verbose=False) + + +def _gpsaf_cmoo(): + """usage_gpsaf_cmoo: GPSAF wrapping NSGA3 on a constrained many-objective problem.""" + ref_dirs = get_reference_directions("das-dennis", 3, n_partitions=3) + algo = GPSAF(NSGA3(ref_dirs, n_offsprings=5), n_initial_doe=12, alpha=2, beta=10, n_max_infills=1) + return minimize(C3DTLZ4(n_var=6), algo, ("n_evals", 13), seed=1, verbose=False) + + +def _gpsaf_constr(): + """usage_gpsaf_constr: GPSAF wrapping ISRES on a constrained single-objective problem.""" + from pymoo.algorithms.soo.nonconvex.isres import ISRES + + algo = GPSAF(ISRES(), n_initial_doe=10, alpha=3, beta=10, n_max_infills=1) + return minimize(get_problem("g1"), algo, ("n_evals", 11), seed=1, verbose=False) + + +def _psaf(): + """usage_psaf: PSAF wrapping DE on a single-objective problem.""" + algo = PSAF(DE(pop_size=10, n_offsprings=5), alpha=5, beta=10) + return minimize(Ackley(n_var=5), algo, ("n_evals", 11), seed=1, verbose=False) + + +def _ssansga2(): + """usage_ssansga2: steady-state surrogate-assisted NSGA-II.""" + algo = SSANSGA2(n_initial_doe=10, n_infills=1, surr_pop_size=20, surr_n_gen=10) + return minimize(ZDT1(n_var=5), algo, ("n_evals", 11), seed=1, verbose=False) + + +def _parego(): + """usage_parego: ParEGO (scalarized multi-objective EGO) on a bi-objective problem.""" + return minimize(ZDT1(n_var=5), ParEGO(n_initial_doe=10), ("n_evals", 11), seed=1, verbose=False) + + +def _krvea(): + """usage_krvea: Kriging-assisted RVEA on a 3-objective problem.""" + from pymoo.problems import get_problem + from pymoo.util.ref_dirs import get_reference_directions + + ref = get_reference_directions("das-dennis", 3, n_partitions=4) + algo = KRVEA(ref_dirs=ref, n_initial_doe=10, n_infills=2, w_max=5) + return minimize(get_problem("dtlz2", n_var=5, n_obj=3), algo, ("n_evals", 13), seed=1, verbose=False) + + +def _ehvi(): + """usage_ehvi: Expected Hypervolume Improvement BO on a bi-objective problem.""" + algo = EHVI(n_initial_doe=10, pool=40, n_screen=6, n_samples=6) + return minimize(ZDT1(n_var=5), algo, ("n_evals", 12), seed=1, verbose=False) + + +def _turbo(): + """usage_turbo: trust-region Bayesian optimization on a single-objective problem.""" + return minimize(Ackley(n_var=5), TuRBO(n_initial_doe=10, n_candidates=50), ("n_evals", 12), seed=1, verbose=False) + + +def _moead_ego(): + """usage_moead_ego: decomposition-based EGO with a batch infill on a bi-objective problem.""" + algo = MOEADEGO(n_initial_doe=10, n_infills=2, pool=40) + return minimize(ZDT1(n_var=5), algo, ("n_evals", 12), seed=1, verbose=False) + + +def _tsemo(): + """usage_tsemo: Thompson-sampling multi-objective BO on a bi-objective problem.""" + return minimize( + ZDT1(n_var=5), TSEMO(n_initial_doe=10, n_infills=2, pool=40), ("n_evals", 12), seed=1, verbose=False + ) + + +def _csea(): + """usage_csea: classification-surrogate-assisted EA on a 3-objective problem.""" + algo = CSEA(n_initial_doe=10, n_infills=2, n_offspring=40) + return minimize(get_problem("dtlz2", n_var=5, n_obj=3), algo, ("n_evals", 13), seed=1, verbose=False) + + +def _saasbo(): + """usage_saasbo: sparse axis-aligned BO (MAP scaffold) on a single-objective problem.""" + return minimize(Ackley(n_var=5), SAASBO(n_initial_doe=10), ("n_evals", 12), seed=1, verbose=False) + + +def _lqcmaes(): + """usage_lqcmaes: surrogate-assisted (local quadratic) CMA-ES.""" + from pysamoo.vendor.lqcmaes import lqCMAES + + return minimize(Sphere(n_var=5), lqCMAES(), ("n_evals", 20), seed=1, verbose=False) + + +def _bo(): + """usage_bayesian_optimization: GP-based Bayesian optimization over one DACE surrogate.""" + from pysamoo.experimental.bo import BayesianOptimization + + return minimize(Sphere(n_var=5), BayesianOptimization(), ("n_gen", 2), seed=1, verbose=False) + + +SCENARIOS = [ + pytest.param(_gpsaf_single, id="gpsaf_single"), + pytest.param(_gpsaf_multi, id="gpsaf_multi"), + pytest.param(_gpsaf_many, id="gpsaf_many"), + pytest.param(_gpsaf_cmoo, id="gpsaf_cmoo"), + pytest.param(_gpsaf_constr, id="gpsaf_constr"), + pytest.param(_psaf, id="psaf"), + pytest.param(_ssansga2, id="ssansga2"), + pytest.param(_parego, id="parego"), + pytest.param(_krvea, id="krvea"), + pytest.param(_ehvi, id="ehvi"), + pytest.param(_turbo, id="turbo"), + pytest.param(_moead_ego, id="moead_ego"), + pytest.param(_tsemo, id="tsemo"), + pytest.param(_csea, id="csea"), + pytest.param(_saasbo, id="saasbo"), + pytest.param(_lqcmaes, id="lqcmaes"), + pytest.param(_bo, id="bayesian_optimization"), +] + + +@pytest.mark.parametrize("build", SCENARIOS) +def test_usage_scenario(build): + """The algorithm runs end-to-end on a minimal budget and evaluates solutions. + + We assert on the final population rather than ``res.F``: on a hard-constrained + problem a tiny budget may not yet contain a feasible point (``res.F`` is then + ``None``), but the run must still have evaluated finite objective values. + """ + res = build() + assert res is not None + assert res.pop is not None and len(res.pop) > 0 + F = res.pop.get("F") + assert F is not None and np.isfinite(F).all() + + +def test_constrained_sampling(): + """usage_constr_sampling: energy-based constrained sampling produces points in-bounds.""" + from pysamoo.sampling.energy import EnergyConstrainedSampling + + problem = SRN() + + def func_constr(X): + G = problem.evaluate(X, return_values_of=["G"]) + return np.maximum(G, 0.0).sum(axis=1) + + X = EnergyConstrainedSampling(func_constr).do(problem, 20).get("X") + xl, xu = problem.bounds() + assert X.shape == (20, problem.n_var) + assert (X >= xl).all() and (X <= xu).all()