From 58f48d359fe7c9c9a77de7ace3e6b79d9c1a7f5e Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Mon, 6 Jan 2025 16:26:02 +0000 Subject: [PATCH 01/31] Add functionality to save and load state of the BayesianOptimization --- bayes_opt/bayesian_optimization.py | 140 +++++++++++++++++++++++++++- tests/test_bayesian_optimization.py | 109 ++++++++++++++++++++++ 2 files changed, 248 insertions(+), 1 deletion(-) diff --git a/bayes_opt/bayesian_optimization.py b/bayes_opt/bayesian_optimization.py index d7f2e4035..649020610 100644 --- a/bayes_opt/bayesian_optimization.py +++ b/bayes_opt/bayesian_optimization.py @@ -10,10 +10,16 @@ from typing import TYPE_CHECKING, Any from warnings import warn +import json +from pathlib import Path +from os import PathLike + import numpy as np from sklearn.gaussian_process import GaussianProcessRegressor from sklearn.gaussian_process.kernels import Matern +from scipy.optimize import NonlinearConstraint + from bayes_opt import acquisition from bayes_opt.constraint import ConstraintModel from bayes_opt.domain_reduction import DomainTransformer @@ -28,7 +34,6 @@ from numpy.random import RandomState from numpy.typing import NDArray - from scipy.optimize import NonlinearConstraint from bayes_opt.acquisition import AcquisitionFunction from bayes_opt.constraint import ConstraintModel @@ -356,3 +361,136 @@ def set_gp_params(self, **params: Any) -> None: if "kernel" in params: params["kernel"] = wrap_kernel(kernel=params["kernel"], transform=self._space.kernel_transform) self._gp.set_params(**params) + + def save_state(self, path: str | PathLike[str]) -> None: + """Save complete state for reconstruction of the optimizer. + + Parameters + ---------- + path : str or PathLike + Path to save the optimization state + """ + random_state = None + if self._random_state is not None: + state_tuple = self._random_state.get_state() + random_state = { + 'bit_generator': state_tuple[0], + 'state': state_tuple[1].tolist(), + 'pos': state_tuple[2], + 'has_gauss': state_tuple[3], + 'cached_gaussian': state_tuple[4], + } + + state = { + "pbounds": { + key: self._space._bounds[i].tolist() + for i, key in enumerate(self._space.keys) + }, + "keys": self._space.keys, + + "params": self._space.params.tolist(), + "target": self._space.target.tolist(), + + "is_constrained": self.is_constrained, + "constraint_values": (self._space._constraint_values.tolist() + if self.is_constrained and self._space._constraint_values is not None + else None), + + "gp_params": { + "kernel": self._gp.kernel.get_params(), + "alpha": self._gp.alpha, + "normalize_y": self._gp.normalize_y, + "n_restarts_optimizer": self._gp.n_restarts_optimizer, + }, + + "allow_duplicate_points": self._allow_duplicate_points, + "verbose": self._verbose, + + "random_state": random_state, + } + + with Path(path).open('w') as f: + json.dump(state, f, indent=2) + + @classmethod + def load(cls, path: str | Path, f: Callable[..., float] | None = None) -> BayesianOptimization: + """Load a saved optimizer state. + + Parameters + ---------- + path : str or Path + Path to the saved state file + f : callable, optional + The function to optimize. If None, the optimizer will be initialized + without a function (useful for analyzing previous results) + + Returns + ------- + BayesianOptimization + A new optimizer instance with the loaded state + """ + with Path(path).open('r') as file: + state = json.load(file) + + # If the original optimizer was constrained, create a dummy constraint + constraint = None + if state["is_constrained"]: + # Create a dummy constraint function that will be replaced by the actual one + def dummy_constraint(*args): return 0 + constraint = NonlinearConstraint(dummy_constraint, -np.inf, np.inf) + + # Initialize a new optimizer with constraint if needed + optimizer = cls( + f=f, + pbounds=state["pbounds"], + random_state=None, + verbose=state["verbose"], + allow_duplicate_points=state["allow_duplicate_points"], + constraint=constraint + ) + + # Restore random state if it was saved + if state["random_state"] is not None: + random_state_tuple = ( + state["random_state"]["bit_generator"], + np.array(state["random_state"]["state"], dtype=np.uint32), + state["random_state"]["pos"], + state["random_state"]["has_gauss"], + state["random_state"]["cached_gaussian"], + ) + optimizer._random_state.set_state(random_state_tuple) + + # Register all previous points + params_array = np.array(state["params"]) + target_array = np.array(state["target"]) + constraint_array = (np.array(state["constraint_values"]) + if state["constraint_values"] is not None + else None) + + for i in range(len(params_array)): + params = optimizer._space.array_to_params(params_array[i]) + target = target_array[i] + constraint = constraint_array[i] if constraint_array is not None else None + optimizer.register( + params=params, + target=target, + constraint_value=constraint, + ) + + # Set GP parameters + optimizer._gp.set_params(**state["gp_params"]) + + # Before fitting the GP, reconstruct the kernel + if isinstance(optimizer._gp.kernel, dict): + kernel_params = optimizer._gp.kernel + optimizer._gp.kernel = Matern( + length_scale=kernel_params['length_scale'], + length_scale_bounds=kernel_params['length_scale_bounds'], + nu=kernel_params['nu'] + ) + + # Only fit GP if there are samples + if len(optimizer._space) > 0: + optimizer._gp.fit(optimizer._space.params, optimizer._space.target) + + return optimizer diff --git a/tests/test_bayesian_optimization.py b/tests/test_bayesian_optimization.py index 48e1af115..c04988bcc 100644 --- a/tests/test_bayesian_optimization.py +++ b/tests/test_bayesian_optimization.py @@ -333,3 +333,112 @@ def test_duplicate_points(): optimizer.register(params=next_point_to_probe, target=target) # and again (should throw warning) optimizer.register(params=next_point_to_probe, target=target) + + +def test_save_load_state(tmp_path): + """Test saving and loading optimizer state.""" + # Initialize and run original optimizer + optimizer = BayesianOptimization( + f=target_func, + pbounds=PBOUNDS, + random_state=1, + verbose=0 + ) + optimizer.maximize(init_points=2, n_iter=3) + + # Save state + state_path = tmp_path / "optimizer_state.json" + optimizer.save_state(state_path) + + # Load state into new optimizer + new_optimizer = BayesianOptimization.load(state_path, f=target_func) + + # Test that key properties match + assert len(optimizer.space) == len(new_optimizer.space) + assert optimizer.max["target"] == new_optimizer.max["target"] + assert optimizer.max["params"] == new_optimizer.max["params"] + np.testing.assert_array_equal(optimizer.space.params, new_optimizer.space.params) + np.testing.assert_array_equal(optimizer.space.target, new_optimizer.space.target) + + +def test_load_without_function(tmp_path): + """Test loading state without providing target function (analysis mode).""" + # Initialize and run original optimizer + optimizer = BayesianOptimization( + f=target_func, + pbounds=PBOUNDS, + random_state=1, + verbose=0 + ) + optimizer.maximize(init_points=2, n_iter=3) + + # Save state + state_path = tmp_path / "optimizer_state.json" + optimizer.save_state(state_path) + + # Load state without function + analysis_optimizer = BayesianOptimization.load(state_path) + + # Should have same history but can't run maximize + assert len(optimizer.space) == len(analysis_optimizer.space) + assert optimizer.max["target"] == analysis_optimizer.max["target"] + with pytest.raises(ValueError, match="No target function has been provided."): + analysis_optimizer.maximize() + + +def test_save_load_with_constraints(tmp_path): + """Test saving and loading state with constraints.""" + from scipy.optimize import NonlinearConstraint + + # Define a simple constraint + def constraint_f(p1, p2): + return p1 + p2 + constraint = NonlinearConstraint(constraint_f, -np.inf, 15) + + # Initialize and run original optimizer + optimizer = BayesianOptimization( + f=target_func, + pbounds=PBOUNDS, + constraint=constraint, + random_state=1, + verbose=0 + ) + optimizer.maximize(init_points=2, n_iter=3) + + # Save state + state_path = tmp_path / "optimizer_state.json" + optimizer.save_state(state_path) + + # Load state into new optimizer + new_optimizer = BayesianOptimization.load(state_path, f=target_func) + + # Test that constraint information is preserved + assert optimizer.is_constrained == new_optimizer.is_constrained + if optimizer.is_constrained: + np.testing.assert_array_equal( + optimizer._space._constraint_values, + new_optimizer._space._constraint_values + ) + + +def test_save_load_random_state(tmp_path): + """Test that random state is properly preserved.""" + # Initialize optimizer + optimizer = BayesianOptimization( + f=target_func, + pbounds=PBOUNDS, + random_state=1, + verbose=0 + ) + + # Save state + state_path = tmp_path / "optimizer_state.json" + optimizer.save_state(state_path) + + # Load state and get next suggestion + new_optimizer = BayesianOptimization.load(state_path, f=target_func) + + # Both optimizers should suggest the same point + suggestion1 = optimizer.suggest() + suggestion2 = new_optimizer.suggest() + assert suggestion1 == suggestion2 From 71036c646c8ef74e3ce643f61e3e74f2a2e0b9c7 Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Mon, 6 Jan 2025 16:26:16 +0000 Subject: [PATCH 02/31] Update basic-tour with new save and load functionality --- examples/basic-tour.ipynb | 231 ++++++++++++++++++++++++++++---------- 1 file changed, 173 insertions(+), 58 deletions(-) diff --git a/examples/basic-tour.ipynb b/examples/basic-tour.ipynb index 4ecd83296..4028f61d2 100644 --- a/examples/basic-tour.ipynb +++ b/examples/basic-tour.ipynb @@ -28,7 +28,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -53,7 +53,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -62,7 +62,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -72,7 +72,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -97,7 +97,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -106,11 +106,11 @@ "text": [ "| iter | target | x | y |\n", "-------------------------------------------------\n", - "| \u001b[0m1 \u001b[0m | \u001b[0m-7.135 \u001b[0m | \u001b[0m2.834 \u001b[0m | \u001b[0m1.322 \u001b[0m |\n", - "| \u001b[0m2 \u001b[0m | \u001b[0m-7.78 \u001b[0m | \u001b[0m2.0 \u001b[0m | \u001b[0m-1.186 \u001b[0m |\n", - "| \u001b[95m3 \u001b[0m | \u001b[95m-6.967 \u001b[0m | \u001b[95m2.582 \u001b[0m | \u001b[95m-0.1396 \u001b[0m |\n", - "| \u001b[0m4 \u001b[0m | \u001b[0m-12.24 \u001b[0m | \u001b[0m2.015 \u001b[0m | \u001b[0m-2.029 \u001b[0m |\n", - "| \u001b[0m5 \u001b[0m | \u001b[0m-18.0 \u001b[0m | \u001b[0m3.302 \u001b[0m | \u001b[0m-1.846 \u001b[0m |\n", + "| \u001b[39m1 \u001b[39m | \u001b[39m-7.135 \u001b[39m | \u001b[39m2.8340440\u001b[39m | \u001b[39m1.3219469\u001b[39m |\n", + "| \u001b[39m2 \u001b[39m | \u001b[39m-7.78 \u001b[39m | \u001b[39m2.0002287\u001b[39m | \u001b[39m-1.186004\u001b[39m |\n", + "| \u001b[39m3 \u001b[39m | \u001b[39m-7.157 \u001b[39m | \u001b[39m2.8375977\u001b[39m | \u001b[39m1.3238498\u001b[39m |\n", + "| \u001b[35m4 \u001b[39m | \u001b[35m-6.633 \u001b[39m | \u001b[35m2.7487090\u001b[39m | \u001b[35m1.2790562\u001b[39m |\n", + "| \u001b[35m5 \u001b[39m | \u001b[35m-5.751 \u001b[39m | \u001b[35m2.5885326\u001b[39m | \u001b[35m1.2246876\u001b[39m |\n", "=================================================\n" ] } @@ -131,14 +131,14 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{'target': -6.966506881014352, 'params': {'x': 2.5822074982517598, 'y': -0.13961016009280103}}\n" + "{'target': np.float64(-5.750985875689304), 'params': {'x': np.float64(2.5885326650623566), 'y': np.float64(1.2246876000015976)}}\n" ] } ], @@ -155,7 +155,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -163,15 +163,15 @@ "output_type": "stream", "text": [ "Iteration 0: \n", - "\t{'target': -7.135455292718879, 'params': {'x': 2.8340440094051482, 'y': 1.3219469606529486}}\n", + "\t{'target': np.float64(-7.135455292718879), 'params': {'x': np.float64(2.8340440094051482), 'y': np.float64(1.3219469606529486)}}\n", "Iteration 1: \n", - "\t{'target': -7.779531005607566, 'params': {'x': 2.0002287496346898, 'y': -1.1860045642089614}}\n", + "\t{'target': np.float64(-7.779531005607566), 'params': {'x': np.float64(2.0002287496346898), 'y': np.float64(-1.1860045642089614)}}\n", "Iteration 2: \n", - "\t{'target': -6.966506881014352, 'params': {'x': 2.5822074982517598, 'y': -0.13961016009280103}}\n", + "\t{'target': np.float64(-7.156839989425082), 'params': {'x': np.float64(2.8375977943744273), 'y': np.float64(1.3238498831039895)}}\n", "Iteration 3: \n", - "\t{'target': -12.235835023240657, 'params': {'x': 2.0154397119682423, 'y': -2.0288343947238228}}\n", + "\t{'target': np.float64(-6.633273772355583), 'params': {'x': np.float64(2.7487090390562576), 'y': np.float64(1.2790562505410115)}}\n", "Iteration 4: \n", - "\t{'target': -17.99963711795217, 'params': {'x': 3.301617813728339, 'y': -1.8458666395359906}}\n" + "\t{'target': np.float64(-5.750985875689304), 'params': {'x': np.float64(2.5885326650623566), 'y': np.float64(1.2246876000015976)}}\n" ] } ], @@ -191,7 +191,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -200,7 +200,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -209,11 +209,11 @@ "text": [ "| iter | target | x | y |\n", "-------------------------------------------------\n", - "| \u001b[95m6 \u001b[0m | \u001b[95m-1.378 \u001b[0m | \u001b[95m1.229 \u001b[0m | \u001b[95m0.06916 \u001b[0m |\n", - "| \u001b[95m7 \u001b[0m | \u001b[95m-0.9471 \u001b[0m | \u001b[95m-0.9434 \u001b[0m | \u001b[95m-0.02816 \u001b[0m |\n", - "| \u001b[0m8 \u001b[0m | \u001b[0m-3.651 \u001b[0m | \u001b[0m-1.168 \u001b[0m | \u001b[0m2.813 \u001b[0m |\n", - "| \u001b[0m9 \u001b[0m | \u001b[0m-17.13 \u001b[0m | \u001b[0m-2.0 \u001b[0m | \u001b[0m-2.758 \u001b[0m |\n", - "| \u001b[95m10 \u001b[0m | \u001b[95m0.8427 \u001b[0m | \u001b[95m-0.193 \u001b[0m | \u001b[95m0.6535 \u001b[0m |\n", + "| \u001b[35m6 \u001b[39m | \u001b[35m-4.438 \u001b[39m | \u001b[35m2.3269441\u001b[39m | \u001b[35m1.1533794\u001b[39m |\n", + "| \u001b[35m7 \u001b[39m | \u001b[35m-2.42 \u001b[39m | \u001b[35m1.8477442\u001b[39m | \u001b[35m0.9230233\u001b[39m |\n", + "| \u001b[35m8 \u001b[39m | \u001b[35m-0.2088 \u001b[39m | \u001b[35m1.0781674\u001b[39m | \u001b[35m1.2152869\u001b[39m |\n", + "| \u001b[35m9 \u001b[39m | \u001b[35m0.7797 \u001b[39m | \u001b[35m-0.298812\u001b[39m | \u001b[35m1.3619705\u001b[39m |\n", + "| \u001b[39m10 \u001b[39m | \u001b[39m-3.391 \u001b[39m | \u001b[39m-0.655060\u001b[39m | \u001b[39m2.9904883\u001b[39m |\n", "=================================================\n" ] } @@ -238,7 +238,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -257,7 +257,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -274,7 +274,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ @@ -286,7 +286,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -295,8 +295,8 @@ "text": [ "| iter | target | x | y |\n", "-------------------------------------------------\n", - "| \u001b[0m11 \u001b[0m | \u001b[0m0.66 \u001b[0m | \u001b[0m0.5 \u001b[0m | \u001b[0m0.7 \u001b[0m |\n", - "| \u001b[0m12 \u001b[0m | \u001b[0m0.1 \u001b[0m | \u001b[0m-0.3 \u001b[0m | \u001b[0m0.1 \u001b[0m |\n", + "| \u001b[39m11 \u001b[39m | \u001b[39m0.66 \u001b[39m | \u001b[39m0.5 \u001b[39m | \u001b[39m0.7 \u001b[39m |\n", + "| \u001b[39m12 \u001b[39m | \u001b[39m0.1 \u001b[39m | \u001b[39m-0.3 \u001b[39m | \u001b[39m0.1 \u001b[39m |\n", "=================================================\n" ] } @@ -318,7 +318,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -341,7 +341,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ @@ -351,7 +351,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 17, "metadata": {}, "outputs": [ { @@ -360,11 +360,11 @@ "text": [ "| iter | target | x | y |\n", "-------------------------------------------------\n", - "| \u001b[0m13 \u001b[0m | \u001b[0m-12.48 \u001b[0m | \u001b[0m-1.266 \u001b[0m | \u001b[0m-2.446 \u001b[0m |\n", - "| \u001b[0m14 \u001b[0m | \u001b[0m-3.854 \u001b[0m | \u001b[0m-1.069 \u001b[0m | \u001b[0m-0.9266 \u001b[0m |\n", - "| \u001b[95m15 \u001b[0m | \u001b[95m0.967 \u001b[0m | \u001b[95m0.04749 \u001b[0m | \u001b[95m1.175 \u001b[0m |\n", - "| \u001b[95m16 \u001b[0m | \u001b[95m0.9912 \u001b[0m | \u001b[95m0.07374 \u001b[0m | \u001b[95m0.9421 \u001b[0m |\n", - "| \u001b[0m17 \u001b[0m | \u001b[0m-3.565 \u001b[0m | \u001b[0m0.7821 \u001b[0m | \u001b[0m2.988 \u001b[0m |\n", + "| \u001b[39m13 \u001b[39m | \u001b[39m-2.96 \u001b[39m | \u001b[39m-1.989407\u001b[39m | \u001b[39m0.9536339\u001b[39m |\n", + "| \u001b[39m14 \u001b[39m | \u001b[39m-0.7135 \u001b[39m | \u001b[39m1.0509704\u001b[39m | \u001b[39m1.7803462\u001b[39m |\n", + "| \u001b[39m15 \u001b[39m | \u001b[39m-18.33 \u001b[39m | \u001b[39m-1.976933\u001b[39m | \u001b[39m-2.927535\u001b[39m |\n", + "| \u001b[35m16 \u001b[39m | \u001b[35m0.9097 \u001b[39m | \u001b[35m-0.228312\u001b[39m | \u001b[35m0.8046706\u001b[39m |\n", + "| \u001b[35m17 \u001b[39m | \u001b[35m0.913 \u001b[39m | \u001b[35m0.2069253\u001b[39m | \u001b[35m1.2101397\u001b[39m |\n", "=================================================\n" ] } @@ -387,7 +387,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 18, "metadata": {}, "outputs": [], "source": [ @@ -396,7 +396,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 19, "metadata": {}, "outputs": [ { @@ -419,16 +419,28 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 20, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/adrianmolzon/oss/BayesianOptimization/bayes_opt/bayesian_optimization.py:240: UserWarning: \n", + "Data point [-1.97693359 -2.9275359 ] is outside the bounds of the parameter y.\n", + "\tBounds:\n", + "[-2. 2.]\n", + " self._space.register(params, target, constraint_value)\n" + ] + } + ], "source": [ "load_logs(new_optimizer, logs=[\"./logs.log\"]);" ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 21, "metadata": {}, "outputs": [ { @@ -445,7 +457,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 22, "metadata": {}, "outputs": [ { @@ -454,33 +466,136 @@ "text": [ "| iter | target | x | y |\n", "-------------------------------------------------\n", - "| \u001b[0m1 \u001b[0m | \u001b[0m-0.6164 \u001b[0m | \u001b[0m-1.271 \u001b[0m | \u001b[0m1.045 \u001b[0m |\n" + "| \u001b[39m1 \u001b[39m | \u001b[39m-7.101 \u001b[39m | \u001b[39m1.9973177\u001b[39m | \u001b[39m-1.027773\u001b[39m |\n", + "| \u001b[39m2 \u001b[39m | \u001b[39m-0.6311 \u001b[39m | \u001b[39m-0.806373\u001b[39m | \u001b[39m1.9903823\u001b[39m |\n", + "| \u001b[39m3 \u001b[39m | \u001b[39m0.2572 \u001b[39m | \u001b[39m0.6921933\u001b[39m | \u001b[39m0.4865326\u001b[39m |\n", + "| \u001b[39m4 \u001b[39m | \u001b[39m-3.0 \u001b[39m | \u001b[39m1.9990210\u001b[39m | \u001b[39m0.9362987\u001b[39m |\n", + "| \u001b[39m5 \u001b[39m | \u001b[39m0.9011 \u001b[39m | \u001b[39m0.1556921\u001b[39m | \u001b[39m0.7268099\u001b[39m |\n", + "| \u001b[39m6 \u001b[39m | \u001b[39m0.9082 \u001b[39m | \u001b[39m-0.238045\u001b[39m | \u001b[39m1.1874540\u001b[39m |\n", + "| \u001b[35m7 \u001b[39m | \u001b[35m0.9988 \u001b[39m | \u001b[35m0.0204153\u001b[39m | \u001b[35m0.9723584\u001b[39m |\n", + "| \u001b[39m8 \u001b[39m | \u001b[39m0.8702 \u001b[39m | \u001b[39m0.3580753\u001b[39m | \u001b[39m0.9606191\u001b[39m |\n", + "| \u001b[39m9 \u001b[39m | \u001b[39m0.997 \u001b[39m | \u001b[39m-0.046534\u001b[39m | \u001b[39m1.0287398\u001b[39m |\n", + "| \u001b[39m10 \u001b[39m | \u001b[39m0.9979 \u001b[39m | \u001b[39m0.0295378\u001b[39m | \u001b[39m1.0346895\u001b[39m |\n", + "=================================================\n" ] - }, + } + ], + "source": [ + "new_optimizer.maximize(\n", + " init_points=0,\n", + " n_iter=10,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Saving and loading the optimizer state\n", + "\n", + "The optimizer state can be saved to a file and loaded from a file. This is useful for continuing an optimization from a previous state, or for analyzing the optimization history without running the optimizer again." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.1 Saving the optimizer state\n", + "\n", + "The optimizer state can be saved to a file using the `save_state` method.\n", + "optimizer.save_state(\"./optimizer_state.json\")" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "optimizer.save_state(\"optimizer_state.json\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5.2 Loading the optimizer state\n", + "\n", + "There are two ways to load a saved state, depending on your needs:\n", + "\n", + "1. With the target function (to continue optimization):\n" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "| \u001b[95m2 \u001b[0m | \u001b[95m0.9988 \u001b[0m | \u001b[95m0.005532 \u001b[0m | \u001b[95m1.034 \u001b[0m |\n", - "| \u001b[0m3 \u001b[0m | \u001b[0m0.9757 \u001b[0m | \u001b[0m0.1501 \u001b[0m | \u001b[0m1.043 \u001b[0m |\n", - "| \u001b[0m4 \u001b[0m | \u001b[0m0.9819 \u001b[0m | \u001b[0m-0.07993 \u001b[0m | \u001b[0m0.8917 \u001b[0m |\n", - "| \u001b[0m5 \u001b[0m | \u001b[0m-3.35 \u001b[0m | \u001b[0m1.976 \u001b[0m | \u001b[0m0.3338 \u001b[0m |\n", - "| \u001b[0m6 \u001b[0m | \u001b[0m0.9778 \u001b[0m | \u001b[0m-0.1166 \u001b[0m | \u001b[0m1.093 \u001b[0m |\n", - "| \u001b[0m7 \u001b[0m | \u001b[0m0.6605 \u001b[0m | \u001b[0m-0.02272 \u001b[0m | \u001b[0m0.4178 \u001b[0m |\n", - "| \u001b[0m8 \u001b[0m | \u001b[0m-0.5044 \u001b[0m | \u001b[0m-0.7146 \u001b[0m | \u001b[0m1.997 \u001b[0m |\n", - "| \u001b[95m9 \u001b[0m | \u001b[95m0.9997 \u001b[0m | \u001b[95m0.0084 \u001b[0m | \u001b[95m1.016 \u001b[0m |\n", - "| \u001b[0m10 \u001b[0m | \u001b[0m-11.83 \u001b[0m | \u001b[0m1.995 \u001b[0m | \u001b[0m-1.974 \u001b[0m |\n", + "| iter | target | x | y |\n", + "-------------------------------------------------\n", + "| \u001b[39m1 \u001b[39m | \u001b[39m0.9988 \u001b[39m | \u001b[39m0.0196213\u001b[39m | \u001b[39m0.9714562\u001b[39m |\n", + "| \u001b[39m2 \u001b[39m | \u001b[39m0.9214 \u001b[39m | \u001b[39m0.0346617\u001b[39m | \u001b[39m0.7218000\u001b[39m |\n", + "| \u001b[39m3 \u001b[39m | \u001b[39m0.9767 \u001b[39m | \u001b[39m-0.116593\u001b[39m | \u001b[39m1.0987579\u001b[39m |\n", + "| \u001b[39m4 \u001b[39m | \u001b[39m0.9967 \u001b[39m | \u001b[39m0.0414245\u001b[39m | \u001b[39m1.0402841\u001b[39m |\n", + "| \u001b[39m5 \u001b[39m | \u001b[39m0.985 \u001b[39m | \u001b[39m0.1168371\u001b[39m | \u001b[39m0.9630685\u001b[39m |\n", "=================================================\n" ] } ], "source": [ + "new_optimizer = BayesianOptimization.load(\n", + " \"optimizer_state.json\",\n", + " f=black_box_function\n", + ")\n", + "\n", + "# Continue optimization\n", "new_optimizer.maximize(\n", " init_points=0,\n", - " n_iter=10,\n", + " n_iter=5\n", ")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "2. Without the target function (for analysis only):\n" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Best result: {'target': np.float64(0.913023173060441), 'params': {'x': np.float64(0.20692536368024994), 'y': np.float64(1.2101397649312364)}}\n", + "All results: 17 points\n" + ] + } + ], + "source": [ + "# Load state for analysis\n", + "analysis_optimizer = BayesianOptimization.load(\"optimizer_state.json\")\n", + "\n", + "# Can inspect results\n", + "print(\"Best result:\", analysis_optimizer.max)\n", + "print(\"All results:\", len(analysis_optimizer.res), \"points\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This provides a simpler alternative to the logging system shown in section 4, especially when you want to continue optimization from a previous state." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -507,7 +622,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.0" + "version": "3.13.1" } }, "nbformat": 4, From 84d036d90f6d03842d870348fa89beb884e52733 Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Tue, 28 Jan 2025 13:39:58 +0000 Subject: [PATCH 03/31] move load stateful path to optional argument in class instantiation --- bayes_opt/bayesian_optimization.py | 151 +++++++++++------------------ 1 file changed, 59 insertions(+), 92 deletions(-) diff --git a/bayes_opt/bayesian_optimization.py b/bayes_opt/bayesian_optimization.py index 649020610..917f0bfbc 100644 --- a/bayes_opt/bayesian_optimization.py +++ b/bayes_opt/bayesian_optimization.py @@ -107,6 +107,9 @@ class BayesianOptimization(Observable): This behavior may be desired in high noise situations where repeatedly probing the same point will give different answers. In other situations, the acquisition may occasionally generate a duplicate point. + + load_state_path: str | Path | None, optional (default=None) + If provided, load optimizer state from this path instead of initializing fresh """ def __init__( @@ -119,6 +122,7 @@ def __init__( verbose: int = 2, bounds_transformer: DomainTransformer | None = None, allow_duplicate_points: bool = False, + load_state_path: str | Path | None = None, ): self._random_state = ensure_rng(random_state) self._allow_duplicate_points = allow_duplicate_points @@ -177,6 +181,55 @@ def __init__( self._sorting_warning_already_shown = False # TODO: remove in future version super().__init__(events=DEFAULT_EVENTS) + if load_state_path is not None: + with Path(load_state_path).open('r') as file: + state = json.load(file) + self._set_state_from_dict(state) + + def _set_state_from_dict(self, state: dict[str, Any]) -> None: + """Set optimizer state from a dictionary of saved values.""" + if state["random_state"] is not None: + random_state_tuple = ( + state["random_state"]["bit_generator"], + np.array(state["random_state"]["state"], dtype=np.uint32), + state["random_state"]["pos"], + state["random_state"]["has_gauss"], + state["random_state"]["cached_gaussian"], + ) + self._random_state.set_state(random_state_tuple) + + self._gp.set_params(**state["gp_params"]) + + # Handle kernel separately since it needs reconstruction + if isinstance(self._gp.kernel, dict): + kernel_params = self._gp.kernel + self._gp.kernel = Matern( + length_scale=kernel_params['length_scale'], + length_scale_bounds=kernel_params['length_scale_bounds'], + nu=kernel_params['nu'] + ) + + # Register previous points + params_array = np.array(state["params"]) + target_array = np.array(state["target"]) + constraint_array = (np.array(state["constraint_values"]) + if state["constraint_values"] is not None + else None) + + for i in range(len(params_array)): + params = self._space.array_to_params(params_array[i]) + target = target_array[i] + constraint = constraint_array[i] if constraint_array is not None else None + self.register( + params=params, + target=target, + constraint_value=constraint, + ) + + # Fit GP if there are samples + if len(self._space) > 0: + self._gp.fit(self._space.params, self._space.target) + @property def space(self) -> TargetSpace: """Return the target space associated with the optimizer.""" @@ -381,116 +434,30 @@ def save_state(self, path: str | PathLike[str]) -> None: 'cached_gaussian': state_tuple[4], } + # Get constraint values if they exist + constraint_values = (self._space._constraint_values.tolist() + if self.is_constrained + else None) + state = { "pbounds": { key: self._space._bounds[i].tolist() for i, key in enumerate(self._space.keys) }, "keys": self._space.keys, - "params": self._space.params.tolist(), "target": self._space.target.tolist(), - - "is_constrained": self.is_constrained, - "constraint_values": (self._space._constraint_values.tolist() - if self.is_constrained and self._space._constraint_values is not None - else None), - + "constraint_values": constraint_values, "gp_params": { "kernel": self._gp.kernel.get_params(), "alpha": self._gp.alpha, "normalize_y": self._gp.normalize_y, "n_restarts_optimizer": self._gp.n_restarts_optimizer, }, - "allow_duplicate_points": self._allow_duplicate_points, "verbose": self._verbose, - "random_state": random_state, } with Path(path).open('w') as f: json.dump(state, f, indent=2) - - @classmethod - def load(cls, path: str | Path, f: Callable[..., float] | None = None) -> BayesianOptimization: - """Load a saved optimizer state. - - Parameters - ---------- - path : str or Path - Path to the saved state file - f : callable, optional - The function to optimize. If None, the optimizer will be initialized - without a function (useful for analyzing previous results) - - Returns - ------- - BayesianOptimization - A new optimizer instance with the loaded state - """ - with Path(path).open('r') as file: - state = json.load(file) - - # If the original optimizer was constrained, create a dummy constraint - constraint = None - if state["is_constrained"]: - # Create a dummy constraint function that will be replaced by the actual one - def dummy_constraint(*args): return 0 - constraint = NonlinearConstraint(dummy_constraint, -np.inf, np.inf) - - # Initialize a new optimizer with constraint if needed - optimizer = cls( - f=f, - pbounds=state["pbounds"], - random_state=None, - verbose=state["verbose"], - allow_duplicate_points=state["allow_duplicate_points"], - constraint=constraint - ) - - # Restore random state if it was saved - if state["random_state"] is not None: - random_state_tuple = ( - state["random_state"]["bit_generator"], - np.array(state["random_state"]["state"], dtype=np.uint32), - state["random_state"]["pos"], - state["random_state"]["has_gauss"], - state["random_state"]["cached_gaussian"], - ) - optimizer._random_state.set_state(random_state_tuple) - - # Register all previous points - params_array = np.array(state["params"]) - target_array = np.array(state["target"]) - constraint_array = (np.array(state["constraint_values"]) - if state["constraint_values"] is not None - else None) - - for i in range(len(params_array)): - params = optimizer._space.array_to_params(params_array[i]) - target = target_array[i] - constraint = constraint_array[i] if constraint_array is not None else None - optimizer.register( - params=params, - target=target, - constraint_value=constraint, - ) - - # Set GP parameters - optimizer._gp.set_params(**state["gp_params"]) - - # Before fitting the GP, reconstruct the kernel - if isinstance(optimizer._gp.kernel, dict): - kernel_params = optimizer._gp.kernel - optimizer._gp.kernel = Matern( - length_scale=kernel_params['length_scale'], - length_scale_bounds=kernel_params['length_scale_bounds'], - nu=kernel_params['nu'] - ) - - # Only fit GP if there are samples - if len(optimizer._space) > 0: - optimizer._gp.fit(optimizer._space.params, optimizer._space.target) - - return optimizer From 6ae61d9c4f39025be7d79b0e06dfd157b893c6b6 Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Tue, 28 Jan 2025 13:41:18 +0000 Subject: [PATCH 04/31] add test for string params, update tests with new load functionality --- tests/test_bayesian_optimization.py | 108 +++++++++++++++++----------- 1 file changed, 67 insertions(+), 41 deletions(-) diff --git a/tests/test_bayesian_optimization.py b/tests/test_bayesian_optimization.py index c04988bcc..37c1c0e94 100644 --- a/tests/test_bayesian_optimization.py +++ b/tests/test_bayesian_optimization.py @@ -351,7 +351,13 @@ def test_save_load_state(tmp_path): optimizer.save_state(state_path) # Load state into new optimizer - new_optimizer = BayesianOptimization.load(state_path, f=target_func) + new_optimizer = BayesianOptimization( + f=target_func, + pbounds=PBOUNDS, + random_state=1, + verbose=0, + load_state_path=state_path + ) # Test that key properties match assert len(optimizer.space) == len(new_optimizer.space) @@ -359,67 +365,81 @@ def test_save_load_state(tmp_path): assert optimizer.max["params"] == new_optimizer.max["params"] np.testing.assert_array_equal(optimizer.space.params, new_optimizer.space.params) np.testing.assert_array_equal(optimizer.space.target, new_optimizer.space.target) - - -def test_load_without_function(tmp_path): - """Test loading state without providing target function (analysis mode).""" - # Initialize and run original optimizer + +def test_save_load_w_string_params(tmp_path): + """Test saving and loading optimizer state with string parameters.""" + def str_target_func(param1: str, param2: str) -> float: + # Simple function that maps strings to numbers + value_map = { + "low": 1.0, + "medium": 2.0, + "high": 3.0 + } + return value_map[param1] + value_map[param2] + + str_pbounds = { + "param1": ["low", "medium", "high"], + "param2": ["low", "medium", "high"] + } + optimizer = BayesianOptimization( - f=target_func, - pbounds=PBOUNDS, + f=str_target_func, + pbounds=str_pbounds, random_state=1, verbose=0 ) + optimizer.maximize(init_points=2, n_iter=3) - # Save state state_path = tmp_path / "optimizer_state.json" optimizer.save_state(state_path) - # Load state without function - analysis_optimizer = BayesianOptimization.load(state_path) + new_optimizer = BayesianOptimization( + f=str_target_func, + pbounds=str_pbounds, + random_state=1, + verbose=0, + load_state_path=state_path + ) - # Should have same history but can't run maximize - assert len(optimizer.space) == len(analysis_optimizer.space) - assert optimizer.max["target"] == analysis_optimizer.max["target"] - with pytest.raises(ValueError, match="No target function has been provided."): - analysis_optimizer.maximize() + assert len(optimizer.space) == len(new_optimizer.space) + assert optimizer.max["target"] == new_optimizer.max["target"] + assert optimizer.max["params"] == new_optimizer.max["params"] + for i in range(len(optimizer.space)): + assert isinstance(optimizer.res[i]["params"]["param1"], str) + assert isinstance(optimizer.res[i]["params"]["param2"], str) + assert isinstance(new_optimizer.res[i]["params"]["param1"], str) + assert isinstance(new_optimizer.res[i]["params"]["param2"], str) + assert optimizer.res[i]["params"] == new_optimizer.res[i]["params"] -def test_save_load_with_constraints(tmp_path): - """Test saving and loading state with constraints.""" - from scipy.optimize import NonlinearConstraint - - # Define a simple constraint - def constraint_f(p1, p2): - return p1 + p2 - constraint = NonlinearConstraint(constraint_f, -np.inf, 15) - - # Initialize and run original optimizer +def test_probe_point_returns_same_point(tmp_path): + """Check that probe returns same point after save/load.""" + # Initialize optimizer optimizer = BayesianOptimization( f=target_func, pbounds=PBOUNDS, - constraint=constraint, random_state=1, verbose=0 ) - optimizer.maximize(init_points=2, n_iter=3) - - # Save state + state_path = tmp_path / "optimizer_state.json" optimizer.save_state(state_path) - - # Load state into new optimizer - new_optimizer = BayesianOptimization.load(state_path, f=target_func) - - # Test that constraint information is preserved - assert optimizer.is_constrained == new_optimizer.is_constrained - if optimizer.is_constrained: - np.testing.assert_array_equal( - optimizer._space._constraint_values, - new_optimizer._space._constraint_values + + new_optimizer = BayesianOptimization( + f=target_func, + pbounds=PBOUNDS, + random_state=1, + verbose=0, + load_state_path=state_path ) + # Both optimizers should probe the same point + point = {"p1": 1.5, "p2": 0.5} + probe1 = optimizer.probe(point) + probe2 = new_optimizer.probe(point) + assert probe1 == probe2 + def test_save_load_random_state(tmp_path): """Test that random state is properly preserved.""" @@ -436,7 +456,13 @@ def test_save_load_random_state(tmp_path): optimizer.save_state(state_path) # Load state and get next suggestion - new_optimizer = BayesianOptimization.load(state_path, f=target_func) + new_optimizer = BayesianOptimization( + f=target_func, + pbounds=PBOUNDS, + random_state=1, + verbose=0, + load_state_path=state_path + ) # Both optimizers should suggest the same point suggestion1 = optimizer.suggest() From 7be3854522027831775a67fdb8f21108c7f098a5 Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Tue, 28 Jan 2025 13:52:17 +0000 Subject: [PATCH 05/31] updated basic tour with updated paths --- examples/basic-tour.ipynb | 79 +++++++-------------------------------- 1 file changed, 14 insertions(+), 65 deletions(-) diff --git a/examples/basic-tour.ipynb b/examples/basic-tour.ipynb index 4028f61d2..cdf85e680 100644 --- a/examples/basic-tour.ipynb +++ b/examples/basic-tour.ipynb @@ -421,19 +421,7 @@ "cell_type": "code", "execution_count": 20, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/adrianmolzon/oss/BayesianOptimization/bayes_opt/bayesian_optimization.py:240: UserWarning: \n", - "Data point [-1.97693359 -2.9275359 ] is outside the bounds of the parameter y.\n", - "\tBounds:\n", - "[-2. 2.]\n", - " self._space.register(params, target, constraint_value)\n" - ] - } - ], + "outputs": [], "source": [ "load_logs(new_optimizer, logs=[\"./logs.log\"]);" ] @@ -521,35 +509,21 @@ "source": [ "## 5.2 Loading the optimizer state\n", "\n", - "There are two ways to load a saved state, depending on your needs:\n", - "\n", - "1. With the target function (to continue optimization):\n" + "To load with a previously saved state, pass the path of your saved state file to the `load_state_path` parameter. Note that if you've changed the bounds of your parameters, you'll need to pass the updated bounds to the new optimizer.\n" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "| iter | target | x | y |\n", - "-------------------------------------------------\n", - "| \u001b[39m1 \u001b[39m | \u001b[39m0.9988 \u001b[39m | \u001b[39m0.0196213\u001b[39m | \u001b[39m0.9714562\u001b[39m |\n", - "| \u001b[39m2 \u001b[39m | \u001b[39m0.9214 \u001b[39m | \u001b[39m0.0346617\u001b[39m | \u001b[39m0.7218000\u001b[39m |\n", - "| \u001b[39m3 \u001b[39m | \u001b[39m0.9767 \u001b[39m | \u001b[39m-0.116593\u001b[39m | \u001b[39m1.0987579\u001b[39m |\n", - "| \u001b[39m4 \u001b[39m | \u001b[39m0.9967 \u001b[39m | \u001b[39m0.0414245\u001b[39m | \u001b[39m1.0402841\u001b[39m |\n", - "| \u001b[39m5 \u001b[39m | \u001b[39m0.985 \u001b[39m | \u001b[39m0.1168371\u001b[39m | \u001b[39m0.9630685\u001b[39m |\n", - "=================================================\n" - ] - } - ], + "outputs": [], "source": [ - "new_optimizer = BayesianOptimization.load(\n", - " \"optimizer_state.json\",\n", - " f=black_box_function\n", + "new_optimizer = BayesianOptimization(\n", + " f=black_box_function,\n", + " pbounds={\"x\": (-2, 3), \"y\": (-3, 3)},\n", + " random_state=1,\n", + " verbose=0,\n", + " load_state_path=\"./optimizer_state.json\"\n", ")\n", "\n", "# Continue optimization\n", @@ -559,36 +533,6 @@ ")" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "2. Without the target function (for analysis only):\n" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Best result: {'target': np.float64(0.913023173060441), 'params': {'x': np.float64(0.20692536368024994), 'y': np.float64(1.2101397649312364)}}\n", - "All results: 17 points\n" - ] - } - ], - "source": [ - "# Load state for analysis\n", - "analysis_optimizer = BayesianOptimization.load(\"optimizer_state.json\")\n", - "\n", - "# Can inspect results\n", - "print(\"Best result:\", analysis_optimizer.max)\n", - "print(\"All results:\", len(analysis_optimizer.res), \"points\")" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -604,6 +548,11 @@ "\n", "This tour should be enough to cover most usage scenarios of this package. If, however, you feel like you need to know more, please checkout the `advanced-tour` notebook. There you will be able to find other, more advanced features of this package that could be what you're looking for. Also, browse the examples folder for implementation tips and ideas." ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] } ], "metadata": { From 2b514aabb8141964549d1c2ab4539ab3a0e1e3f6 Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Thu, 6 Feb 2025 16:24:28 +0000 Subject: [PATCH 06/31] add the random state to the set of things to list of saved items --- bayes_opt/acquisition.py | 152 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 143 insertions(+), 9 deletions(-) diff --git a/bayes_opt/acquisition.py b/bayes_opt/acquisition.py index 167bd5dc0..9675cb5a8 100644 --- a/bayes_opt/acquisition.py +++ b/bayes_opt/acquisition.py @@ -69,18 +69,57 @@ def __init__(self, random_state: int | RandomState | None = None) -> None: self.random_state = RandomState() self.i = 0 + def _serialize_random_state(self) -> dict | None: + """Convert random state to JSON serializable format.""" + if self.random_state is not None: + state = self.random_state.get_state() + return { + 'bit_generator': state[0], + 'state': state[1].tolist(), # Convert numpy array to list + 'pos': state[2], + 'has_gauss': state[3], + 'cached_gaussian': state[4] + } + return None + + def _deserialize_random_state(self, state_dict: dict | None) -> None: + """Restore random state from JSON serializable format.""" + if state_dict is not None: + if self.random_state is None: + self.random_state = RandomState() + state = ( + state_dict['bit_generator'], + np.array(state_dict['state'], dtype=np.uint32), + state_dict['pos'], + state_dict['has_gauss'], + state_dict['cached_gaussian'] + ) + self.random_state.set_state(state) + @abc.abstractmethod def base_acq(self, *args: Any, **kwargs: Any) -> NDArray[Float]: """Provide access to the base acquisition function.""" - - def _fit_gp(self, gp: GaussianProcessRegressor, target_space: TargetSpace) -> None: - # Sklearn's GP throws a large number of warnings at times, but - # we don't really need to see them here. - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - gp.fit(target_space.params, target_space.target) - if target_space.constraint is not None: - target_space.constraint.fit(target_space.params, target_space._constraint_values) + + @abc.abstractmethod + def get_acquisition_params(self) -> dict[str, Any]: + """Get the acquisition function parameters. + + Returns + ------- + dict + Dictionary containing the acquisition function parameters. + All values must be JSON serializable. + """ + + @abc.abstractmethod + def set_acquisition_params(self, params: dict[str, Any]) -> None: + """Set the acquisition function parameters. + + Parameters + ---------- + params : dict + Dictionary containing the acquisition function parameters. + """ def suggest( self, @@ -128,6 +167,15 @@ def suggest( acq = self._get_acq(gp=gp, constraint=target_space.constraint) return self._acq_min(acq, target_space, n_random=n_random, n_l_bfgs_b=n_l_bfgs_b) + + def _fit_gp(self, gp: GaussianProcessRegressor, target_space: TargetSpace) -> None: + # Sklearn's GP throws a large number of warnings at times, but + # we don't really need to see them here. + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + gp.fit(target_space.params, target_space.target) + if target_space.constraint is not None: + target_space.constraint.fit(target_space.params, target_space._constraint_values) def _get_acq( self, gp: GaussianProcessRegressor, constraint: ConstraintModel | None = None @@ -453,6 +501,20 @@ def decay_exploration(self) -> None: self.exploration_decay_delay is None or self.exploration_decay_delay <= self.i ): self.kappa = self.kappa * self.exploration_decay + + def get_acquisition_params(self) -> dict: + return { + "kappa": self.kappa, + "exploration_decay": self.exploration_decay, + "exploration_decay_delay": self.exploration_decay_delay, + "random_state": self._serialize_random_state() + } + + def set_acquisition_params(self, params: dict) -> None: + self.kappa = params["kappa"] + self.exploration_decay = params["exploration_decay"] + self.exploration_decay_delay = params["exploration_decay_delay"] + self._deserialize_random_state(params["random_state"]) class ProbabilityOfImprovement(AcquisitionFunction): @@ -586,6 +648,21 @@ def decay_exploration(self) -> None: self.exploration_decay_delay is None or self.exploration_decay_delay <= self.i ): self.xi = self.xi * self.exploration_decay + + def get_acquisition_params(self) -> dict: + """Get the acquisition function parameters.""" + return { + "xi": self.xi, + "exploration_decay": self.exploration_decay, + "exploration_decay_delay": self.exploration_decay_delay, + "random_state": self._serialize_random_state() + } + + def set_acquisition_params(self, params: dict) -> None: + self.xi = params["xi"] + self.exploration_decay = params["exploration_decay"] + self.exploration_decay_delay = params["exploration_decay_delay"] + self._deserialize_random_state(params["random_state"]) class ExpectedImprovement(AcquisitionFunction): @@ -727,6 +804,20 @@ def decay_exploration(self) -> None: self.exploration_decay_delay is None or self.exploration_decay_delay <= self.i ): self.xi = self.xi * self.exploration_decay + + def get_acquisition_params(self) -> dict: + return { + "xi": self.xi, + "exploration_decay": self.exploration_decay, + "exploration_decay_delay": self.exploration_decay_delay, + "random_state": self._serialize_random_state() + } + + def set_acquisition_params(self, params: dict) -> None: + self.xi = params["xi"] + self.exploration_decay = params["exploration_decay"] + self.exploration_decay_delay = params["exploration_decay_delay"] + self._deserialize_random_state(params["random_state"]) class ConstantLiar(AcquisitionFunction): @@ -917,6 +1008,24 @@ def suggest( self.dummies.append(x_max) return x_max + + def get_acquisition_params(self) -> dict: + return { + "dummies": [dummy.tolist() for dummy in self.dummies], + "base_acquisition_params": self.base_acquisition.get_acquisition_params(), + "strategy": self.strategy, + "atol": self.atol, + "rtol": self.rtol, + "random_state": self._serialize_random_state() + } + + def set_acquisition_params(self, params: dict) -> None: + self.dummies = [np.array(dummy) for dummy in params["dummies"]] + self.base_acquisition.set_acquisition_params(params["base_acquisition_params"]) + self.strategy = params["strategy"] + self.atol = params["atol"] + self.rtol = params["rtol"] + self._deserialize_random_state(params["random_state"]) class GPHedge(AcquisitionFunction): @@ -1035,3 +1144,28 @@ def suggest( self.previous_candidates = np.array(x_max) idx = self._sample_idx_from_softmax_gains() return x_max[idx] + + def get_acquisition_params(self) -> dict: + return { + "base_acquisitions_params": [acq.get_acquisition_params() for acq in self.base_acquisitions], + "gains": self.gains.tolist(), + "previous_candidates": self.previous_candidates.tolist() if self.previous_candidates is not None else None, + "random_states": [acq._serialize_random_state() for acq in self.base_acquisitions] + [self._serialize_random_state()] + } + + def set_acquisition_params(self, params: dict) -> None: + for acq, acq_params, random_state in zip( + self.base_acquisitions, + params["base_acquisitions_params"], + params["random_states"][:-1] + ): + acq.set_acquisition_params(acq_params) + acq._deserialize_random_state(random_state) + + self.gains = np.array(params["gains"]) + self.previous_candidates = (np.array(params["previous_candidates"]) + if params["previous_candidates"] is not None + else None) + + self._deserialize_random_state(params["random_states"][-1]) + From be7326227c7dd65fa348944e829685a08c4a4ba9 Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Thu, 6 Feb 2025 16:25:31 +0000 Subject: [PATCH 07/31] move state loading to separate function, add functionality for saving acquisition function state --- bayes_opt/bayesian_optimization.py | 101 +++++++++++++---------------- 1 file changed, 46 insertions(+), 55 deletions(-) diff --git a/bayes_opt/bayesian_optimization.py b/bayes_opt/bayesian_optimization.py index 917f0bfbc..c9450e743 100644 --- a/bayes_opt/bayesian_optimization.py +++ b/bayes_opt/bayesian_optimization.py @@ -107,9 +107,6 @@ class BayesianOptimization(Observable): This behavior may be desired in high noise situations where repeatedly probing the same point will give different answers. In other situations, the acquisition may occasionally generate a duplicate point. - - load_state_path: str | Path | None, optional (default=None) - If provided, load optimizer state from this path instead of initializing fresh """ def __init__( @@ -122,7 +119,6 @@ def __init__( verbose: int = 2, bounds_transformer: DomainTransformer | None = None, allow_duplicate_points: bool = False, - load_state_path: str | Path | None = None, ): self._random_state = ensure_rng(random_state) self._allow_duplicate_points = allow_duplicate_points @@ -181,55 +177,6 @@ def __init__( self._sorting_warning_already_shown = False # TODO: remove in future version super().__init__(events=DEFAULT_EVENTS) - if load_state_path is not None: - with Path(load_state_path).open('r') as file: - state = json.load(file) - self._set_state_from_dict(state) - - def _set_state_from_dict(self, state: dict[str, Any]) -> None: - """Set optimizer state from a dictionary of saved values.""" - if state["random_state"] is not None: - random_state_tuple = ( - state["random_state"]["bit_generator"], - np.array(state["random_state"]["state"], dtype=np.uint32), - state["random_state"]["pos"], - state["random_state"]["has_gauss"], - state["random_state"]["cached_gaussian"], - ) - self._random_state.set_state(random_state_tuple) - - self._gp.set_params(**state["gp_params"]) - - # Handle kernel separately since it needs reconstruction - if isinstance(self._gp.kernel, dict): - kernel_params = self._gp.kernel - self._gp.kernel = Matern( - length_scale=kernel_params['length_scale'], - length_scale_bounds=kernel_params['length_scale_bounds'], - nu=kernel_params['nu'] - ) - - # Register previous points - params_array = np.array(state["params"]) - target_array = np.array(state["target"]) - constraint_array = (np.array(state["constraint_values"]) - if state["constraint_values"] is not None - else None) - - for i in range(len(params_array)): - params = self._space.array_to_params(params_array[i]) - target = target_array[i] - constraint = constraint_array[i] if constraint_array is not None else None - self.register( - params=params, - target=target, - constraint_value=constraint, - ) - - # Fit GP if there are samples - if len(self._space) > 0: - self._gp.fit(self._space.params, self._space.target) - @property def space(self) -> TargetSpace: """Return the target space associated with the optimizer.""" @@ -433,12 +380,12 @@ def save_state(self, path: str | PathLike[str]) -> None: 'has_gauss': state_tuple[3], 'cached_gaussian': state_tuple[4], } - + # Get constraint values if they exist constraint_values = (self._space._constraint_values.tolist() if self.is_constrained else None) - + acquisition_params = self._acquisition_function.get_acquisition_params() state = { "pbounds": { key: self._space._bounds[i].tolist() @@ -457,7 +404,51 @@ def save_state(self, path: str | PathLike[str]) -> None: "allow_duplicate_points": self._allow_duplicate_points, "verbose": self._verbose, "random_state": random_state, + "acquisition_params": acquisition_params, } with Path(path).open('w') as f: json.dump(state, f, indent=2) + + def load_state(self, path: str | PathLike[str]) -> None: + with Path(path).open('r') as file: + state = json.load(file) + + if state["random_state"] is not None: + random_state_tuple = ( + state["random_state"]["bit_generator"], + np.array(state["random_state"]["state"], dtype=np.uint32), + state["random_state"]["pos"], + state["random_state"]["has_gauss"], + state["random_state"]["cached_gaussian"], + ) + self._random_state.set_state(random_state_tuple) + + self._gp.set_params(**state["gp_params"]) + + if isinstance(self._gp.kernel, dict): + kernel_params = self._gp.kernel + self._gp.kernel = Matern( + length_scale=kernel_params['length_scale'], + length_scale_bounds=tuple(kernel_params['length_scale_bounds']), + nu=kernel_params['nu'] + ) + + params_array = np.array(state["params"]) + target_array = np.array(state["target"]) + constraint_array = (np.array(state["constraint_values"]) + if state["constraint_values"] is not None + else None) + + for i in range(len(params_array)): + params = self._space.array_to_params(params_array[i]) + target = target_array[i] + constraint = constraint_array[i] if constraint_array is not None else None + self.register( + params=params, + target=target, + constraint_value=constraint + ) + + self._acquisition_function.set_acquisition_params(state["acquisition_params"]) + self._gp.fit(self._space.params, self._space.target) From ad2c8221f93769232c6f37373151e3689fc8741c Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Thu, 6 Feb 2025 16:52:56 +0000 Subject: [PATCH 08/31] use new loading schema --- examples/basic-tour.ipynb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/basic-tour.ipynb b/examples/basic-tour.ipynb index cdf85e680..9761deaff 100644 --- a/examples/basic-tour.ipynb +++ b/examples/basic-tour.ipynb @@ -522,10 +522,11 @@ " f=black_box_function,\n", " pbounds={\"x\": (-2, 3), \"y\": (-3, 3)},\n", " random_state=1,\n", - " verbose=0,\n", - " load_state_path=\"./optimizer_state.json\"\n", + " verbose=0\n", ")\n", "\n", + "new_optimizer.load_state(\"./optimizer_state.json\")\n", + "\n", "# Continue optimization\n", "new_optimizer.maximize(\n", " init_points=0,\n", From aa86e2f3c58285f2198f6df8023f7e46eb7790cd Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Thu, 6 Feb 2025 16:54:08 +0000 Subject: [PATCH 09/31] update tests, add integration tests for saving and loading acquisition functions --- tests/test_acquisition.py | 6 + tests/test_bayesian_optimization.py | 134 +++++++++++++-- tests/test_integration.py | 250 ++++++++++++++++++++++++++++ 3 files changed, 376 insertions(+), 14 deletions(-) create mode 100644 tests/test_integration.py diff --git a/tests/test_acquisition.py b/tests/test_acquisition.py index 1191976df..12217c59a 100644 --- a/tests/test_acquisition.py +++ b/tests/test_acquisition.py @@ -53,6 +53,12 @@ def mock_acq(x: np.ndarray): def base_acq(self, mean, std): pass + def get_acquisition_params(self) -> dict: + return {} + + def set_acquisition_params(self, params: dict) -> None: + pass + def test_base_acquisition(): acq = acquisition.UpperConfidenceBound() diff --git a/tests/test_bayesian_optimization.py b/tests/test_bayesian_optimization.py index 37c1c0e94..2831e40d3 100644 --- a/tests/test_bayesian_optimization.py +++ b/tests/test_bayesian_optimization.py @@ -12,6 +12,7 @@ from bayes_opt.exception import NotUniqueError from bayes_opt.logger import ScreenLogger from bayes_opt.target_space import TargetSpace +from scipy.optimize import NonlinearConstraint def target_func(**kwargs): @@ -350,14 +351,14 @@ def test_save_load_state(tmp_path): state_path = tmp_path / "optimizer_state.json" optimizer.save_state(state_path) - # Load state into new optimizer + # Create new optimizer and load state new_optimizer = BayesianOptimization( f=target_func, pbounds=PBOUNDS, random_state=1, - verbose=0, - load_state_path=state_path + verbose=0 ) + new_optimizer.load_state(state_path) # Test that key properties match assert len(optimizer.space) == len(new_optimizer.space) @@ -365,9 +366,10 @@ def test_save_load_state(tmp_path): assert optimizer.max["params"] == new_optimizer.max["params"] np.testing.assert_array_equal(optimizer.space.params, new_optimizer.space.params) np.testing.assert_array_equal(optimizer.space.target, new_optimizer.space.target) - -def test_save_load_w_string_params(tmp_path): - """Test saving and loading optimizer state with string parameters.""" + + +def test_save_load_w_categorical_params(tmp_path): + """Test saving and loading optimizer state with categorical parameters.""" def str_target_func(param1: str, param2: str) -> float: # Simple function that maps strings to numbers value_map = { @@ -398,9 +400,9 @@ def str_target_func(param1: str, param2: str) -> float: f=str_target_func, pbounds=str_pbounds, random_state=1, - verbose=0, - load_state_path=state_path + verbose=0 ) + new_optimizer.load_state(state_path) assert len(optimizer.space) == len(new_optimizer.space) assert optimizer.max["target"] == new_optimizer.max["target"] @@ -415,7 +417,6 @@ def str_target_func(param1: str, param2: str) -> float: def test_probe_point_returns_same_point(tmp_path): """Check that probe returns same point after save/load.""" - # Initialize optimizer optimizer = BayesianOptimization( f=target_func, pbounds=PBOUNDS, @@ -423,6 +424,11 @@ def test_probe_point_returns_same_point(tmp_path): verbose=0 ) + optimizer.register( + params={"p1": 5.0, "p2": 5.0}, + target=10.0 + ) + state_path = tmp_path / "optimizer_state.json" optimizer.save_state(state_path) @@ -430,9 +436,9 @@ def test_probe_point_returns_same_point(tmp_path): f=target_func, pbounds=PBOUNDS, random_state=1, - verbose=0, - load_state_path=state_path - ) + verbose=0 + ) + new_optimizer.load_state(state_path) # Both optimizers should probe the same point point = {"p1": 1.5, "p2": 0.5} @@ -441,6 +447,33 @@ def test_probe_point_returns_same_point(tmp_path): assert probe1 == probe2 +def test_suggest_point_returns_same_point(tmp_path): + """Check that suggest returns same point after save/load.""" + optimizer = BayesianOptimization( + f=target_func, + pbounds=PBOUNDS, + random_state=1, + verbose=0 + ) + optimizer.maximize(init_points=2, n_iter=3) + + state_path = tmp_path / "optimizer_state.json" + optimizer.save_state(state_path) + + new_optimizer = BayesianOptimization( + f=target_func, + pbounds=PBOUNDS, + random_state=1, + verbose=0 + ) + new_optimizer.load_state(state_path) + + # Both optimizers should suggest the same point + suggestion1 = optimizer.suggest() + suggestion2 = new_optimizer.suggest() + assert suggestion1 == suggestion2 + + def test_save_load_random_state(tmp_path): """Test that random state is properly preserved.""" # Initialize optimizer @@ -460,11 +493,84 @@ def test_save_load_random_state(tmp_path): f=target_func, pbounds=PBOUNDS, random_state=1, - verbose=0, - load_state_path=state_path + verbose=0 ) # Both optimizers should suggest the same point suggestion1 = optimizer.suggest() suggestion2 = new_optimizer.suggest() assert suggestion1 == suggestion2 + + +def test_save_load_w_constraint(tmp_path): + """Test saving and loading optimizer state with constraints.""" + def constraint_func(x: float, y: float) -> float: + return x + y # Simple constraint: sum of parameters should be within bounds + + constraint = NonlinearConstraint( + fun=constraint_func, + lb=0.0, + ub=3.0 + ) + + # Initialize optimizer with constraint + optimizer = BayesianOptimization( + f=target_func, + pbounds={"x": (-1, 3), "y": (0, 5)}, + constraint=constraint, + random_state=1, + verbose=0 + ) + + # Register some points, some that satisfy constraint and some that don't + optimizer.register( + params={"x": 1.0, "y": 1.0}, # Satisfies constraint: sum = 2.0 + target=2.0, + constraint_value=2.0 + ) + optimizer.register( + params={"x": 2.0, "y": 2.0}, # Violates constraint: sum = 4.0 + target=4.0, + constraint_value=4.0 + ) + optimizer.register( + params={"x": 0.5, "y": 0.5}, # Satisfies constraint: sum = 1.0 + target=1.0, + constraint_value=1.0 + ) + + state_path = tmp_path / "optimizer_state.json" + optimizer.save_state(state_path) + + new_optimizer = BayesianOptimization( + f=target_func, + pbounds={"x": (-1, 3), "y": (0, 5)}, + constraint=constraint, + random_state=1, + verbose=0 + ) + new_optimizer.load_state(state_path) + + # Test that key properties match + assert len(optimizer.space) == len(new_optimizer.space) + assert optimizer.max["target"] == new_optimizer.max["target"] + assert optimizer.max["params"] == new_optimizer.max["params"] + np.testing.assert_array_equal(optimizer.space.params, new_optimizer.space.params) + np.testing.assert_array_equal(optimizer.space.target, new_optimizer.space.target) + + # Test that constraint values were properly saved and loaded + np.testing.assert_array_equal( + optimizer.space._constraint_values, + new_optimizer.space._constraint_values + ) + + # Test that both optimizers suggest the same point (should respect constraints) + suggestion1 = optimizer.suggest() + suggestion2 = new_optimizer.suggest() + assert suggestion1 == suggestion2 + + # Verify that suggested point satisfies constraint + constraint_value = constraint_func(**suggestion1) + assert 0.0 <= constraint_value <= 3.0, "Suggested point violates constraint" + + diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 000000000..076995677 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,250 @@ +from __future__ import annotations + +import numpy as np +import pytest +from scipy.optimize import NonlinearConstraint +import json + +from bayes_opt import BayesianOptimization +from bayes_opt.acquisition import ( + UpperConfidenceBound, + ProbabilityOfImprovement, + ExpectedImprovement, + ConstantLiar, + GPHedge +) + +# Test fixtures +@pytest.fixture +def target_func(): + return lambda x, y: -(x - 1)**2 - (y - 2)**2 # Maximum at (1,2) + +@pytest.fixture +def pbounds(): + return {"x": (-5, 5), "y": (-5, 5)} + +@pytest.fixture +def constraint_func(): + return lambda x, y: x + y # Simple constraint: sum of parameters + +@pytest.fixture +def constraint(constraint_func): + return NonlinearConstraint( + fun=constraint_func, + lb=-1.0, + ub=4.0 + ) + +def verify_optimizers_match(optimizer1, optimizer2): + """Helper function to verify two optimizers match.""" + assert len(optimizer1.space) == len(optimizer2.space) + assert optimizer1.max["target"] == optimizer2.max["target"] + assert optimizer1.max["params"] == optimizer2.max["params"] + + + + np.testing.assert_array_equal(optimizer1.space.params, optimizer2.space.params) + np.testing.assert_array_equal(optimizer1.space.target, optimizer2.space.target) + + if optimizer1.is_constrained: + np.testing.assert_array_equal( + optimizer1.space._constraint_values, + optimizer2.space._constraint_values + ) + assert optimizer1.space._constraint.lb == optimizer2.space._constraint.lb + assert optimizer1.space._constraint.ub == optimizer2.space._constraint.ub + + assert np.random.get_state()[1][0] == np.random.get_state()[1][0] + + assert optimizer1._gp.kernel.get_params() == optimizer2._gp.kernel.get_params() + + suggestion1 = optimizer1.suggest() + suggestion2 = optimizer2.suggest() + assert suggestion1 == suggestion2, f"\nSuggestion 1: {suggestion1}\nSuggestion 2: {suggestion2}" + + + + +def test_integration_upper_confidence_bound(target_func, pbounds, tmp_path): + """Test save/load integration with UpperConfidenceBound acquisition.""" + acquisition_function = UpperConfidenceBound(kappa=2.576) + + # Create and run first optimizer + optimizer = BayesianOptimization( + f=target_func, + pbounds=pbounds, + acquisition_function=acquisition_function, + random_state=1, + verbose=0 + ) + optimizer.maximize(init_points=2, n_iter=3) + + # Save state + state_path = tmp_path / "ucb_state.json" + optimizer.save_state(state_path) + + # Create new optimizer and load state + new_optimizer = BayesianOptimization( + f=target_func, + pbounds=pbounds, + acquisition_function=UpperConfidenceBound(kappa=2.576), + random_state=1, + verbose=0 + ) + new_optimizer.load_state(state_path) + + verify_optimizers_match(optimizer, new_optimizer) + +def test_integration_probability_improvement(target_func, pbounds, tmp_path): + """Test save/load integration with ProbabilityOfImprovement acquisition.""" + acquisition_function = ProbabilityOfImprovement(xi=0.01) + + optimizer = BayesianOptimization( + f=target_func, + pbounds=pbounds, + acquisition_function=acquisition_function, + random_state=1, + verbose=0 + ) + optimizer.maximize(init_points=2, n_iter=3) + + state_path = tmp_path / "pi_state.json" + optimizer.save_state(state_path) + + new_optimizer = BayesianOptimization( + f=target_func, + pbounds=pbounds, + acquisition_function=ProbabilityOfImprovement(xi=0.01), + random_state=1, + verbose=0 + ) + new_optimizer.load_state(state_path) + + verify_optimizers_match(optimizer, new_optimizer) + +def test_integration_expected_improvement(target_func, pbounds, tmp_path): + """Test save/load integration with ExpectedImprovement acquisition.""" + acquisition_function = ExpectedImprovement(xi=0.01) + + optimizer = BayesianOptimization( + f=target_func, + pbounds=pbounds, + acquisition_function=acquisition_function, + random_state=1, + verbose=0 + ) + optimizer.maximize(init_points=2, n_iter=3) + + state_path = tmp_path / "ei_state.json" + optimizer.save_state(state_path) + + new_optimizer = BayesianOptimization( + f=target_func, + pbounds=pbounds, + acquisition_function=ExpectedImprovement(xi=0.01), + random_state=1, + verbose=0 + ) + new_optimizer.load_state(state_path) + + verify_optimizers_match(optimizer, new_optimizer) + +def test_integration_constant_liar(target_func, pbounds, tmp_path): + """Test save/load integration with ConstantLiar acquisition.""" + base_acq = UpperConfidenceBound(kappa=2.576) + acquisition_function = ConstantLiar(base_acquisition=base_acq) + + optimizer = BayesianOptimization( + f=target_func, + pbounds=pbounds, + acquisition_function=acquisition_function, + random_state=1, + verbose=0 + ) + optimizer.maximize(init_points=2, n_iter=3) + + state_path = tmp_path / "cl_state.json" + optimizer.save_state(state_path) + + new_optimizer = BayesianOptimization( + f=target_func, + pbounds=pbounds, + acquisition_function=ConstantLiar(base_acquisition=UpperConfidenceBound(kappa=2.576)), + random_state=1, + verbose=0 + ) + new_optimizer.load_state(state_path) + + verify_optimizers_match(optimizer, new_optimizer) + +def test_integration_gp_hedge(target_func, pbounds, tmp_path): + """Test save/load integration with GPHedge acquisition.""" + base_acquisitions = [ + UpperConfidenceBound(kappa=2.576), + ProbabilityOfImprovement(xi=0.01), + ExpectedImprovement(xi=0.01) + ] + acquisition_function = GPHedge(base_acquisitions=base_acquisitions) + + optimizer = BayesianOptimization( + f=target_func, + pbounds=pbounds, + acquisition_function=acquisition_function, + random_state=1, + verbose=0 + ) + optimizer.maximize(init_points=2, n_iter=3) + + state_path = tmp_path / "gphedge_state.json" + optimizer.save_state(state_path) + + new_base_acquisitions = [ + UpperConfidenceBound(kappa=2.576), + ProbabilityOfImprovement(xi=0.01), + ExpectedImprovement(xi=0.01) + ] + new_optimizer = BayesianOptimization( + f=target_func, + pbounds=pbounds, + acquisition_function=GPHedge(base_acquisitions=new_base_acquisitions), + random_state=1, + verbose=0 + ) + new_optimizer.load_state(state_path) + + # Print new optimizer state + print("\nNew Optimizer State:") + print(f"GP random state: {new_optimizer._gp.random_state}") + print(f"GP kernel params:\n{new_optimizer._gp.kernel_.get_params()}") + print(f"Global random state: {np.random.get_state()[1][0]}") + + verify_optimizers_match(optimizer, new_optimizer) + +def test_integration_constrained(target_func, pbounds, constraint, tmp_path): + """Test save/load integration with constraints.""" + acquisition_function = ExpectedImprovement(xi=0.01) + + optimizer = BayesianOptimization( + f=target_func, + pbounds=pbounds, + acquisition_function=acquisition_function, + constraint=constraint, + random_state=1, + verbose=0 + ) + optimizer.maximize(init_points=2, n_iter=3) + + state_path = tmp_path / "constrained_state.json" + optimizer.save_state(state_path) + + new_optimizer = BayesianOptimization( + f=target_func, + pbounds=pbounds, + acquisition_function=ExpectedImprovement(xi=0.01), + constraint=constraint, + random_state=1, + verbose=0 + ) + new_optimizer.load_state(state_path) + + verify_optimizers_match(optimizer, new_optimizer) From c26504e9a88fdfad7f1a1f5ece6e5a2efcde1409 Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Thu, 6 Feb 2025 17:03:12 +0000 Subject: [PATCH 10/31] undo abstractmethod implementation for get and set state saving functionality --- bayes_opt/acquisition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bayes_opt/acquisition.py b/bayes_opt/acquisition.py index 9675cb5a8..b60ca1b34 100644 --- a/bayes_opt/acquisition.py +++ b/bayes_opt/acquisition.py @@ -99,8 +99,7 @@ def _deserialize_random_state(self, state_dict: dict | None) -> None: @abc.abstractmethod def base_acq(self, *args: Any, **kwargs: Any) -> NDArray[Float]: """Provide access to the base acquisition function.""" - - @abc.abstractmethod + def get_acquisition_params(self) -> dict[str, Any]: """Get the acquisition function parameters. @@ -110,8 +109,8 @@ def get_acquisition_params(self) -> dict[str, Any]: Dictionary containing the acquisition function parameters. All values must be JSON serializable. """ + return {} - @abc.abstractmethod def set_acquisition_params(self, params: dict[str, Any]) -> None: """Set the acquisition function parameters. @@ -120,6 +119,7 @@ def set_acquisition_params(self, params: dict[str, Any]) -> None: params : dict Dictionary containing the acquisition function parameters. """ + pass def suggest( self, From 4aab0b89aa954bba5503a5fa52766a4011f9871b Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Mon, 10 Feb 2025 13:54:28 +0000 Subject: [PATCH 11/31] reorganize state saving and loading for consistency --- bayes_opt/bayesian_optimization.py | 76 ++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/bayes_opt/bayesian_optimization.py b/bayes_opt/bayesian_optimization.py index c9450e743..17976721a 100644 --- a/bayes_opt/bayesian_optimization.py +++ b/bayes_opt/bayesian_optimization.py @@ -369,7 +369,18 @@ def save_state(self, path: str | PathLike[str]) -> None: ---------- path : str or PathLike Path to save the optimization state + + Raises + ------ + ValueError + If attempting to save state before collecting any samples. """ + if len(self._space) == 0: + raise ValueError( + "Cannot save optimizer state before collecting any samples. " + "Please probe or register at least one point before saving." + ) + random_state = None if self._random_state is not None: state_tuple = self._random_state.get_state() @@ -391,8 +402,14 @@ def save_state(self, path: str | PathLike[str]) -> None: key: self._space._bounds[i].tolist() for i, key in enumerate(self._space.keys) }, + # Add current transformed bounds if using bounds transformer + "transformed_bounds": ( + self._space.bounds.tolist() + if self._bounds_transformer + else None + ), "keys": self._space.keys, - "params": self._space.params.tolist(), + "params": np.array(self._space.params).tolist(), "target": self._space.target.tolist(), "constraint_values": constraint_values, "gp_params": { @@ -414,28 +431,8 @@ def load_state(self, path: str | PathLike[str]) -> None: with Path(path).open('r') as file: state = json.load(file) - if state["random_state"] is not None: - random_state_tuple = ( - state["random_state"]["bit_generator"], - np.array(state["random_state"]["state"], dtype=np.uint32), - state["random_state"]["pos"], - state["random_state"]["has_gauss"], - state["random_state"]["cached_gaussian"], - ) - self._random_state.set_state(random_state_tuple) - - self._gp.set_params(**state["gp_params"]) - - if isinstance(self._gp.kernel, dict): - kernel_params = self._gp.kernel - self._gp.kernel = Matern( - length_scale=kernel_params['length_scale'], - length_scale_bounds=tuple(kernel_params['length_scale_bounds']), - nu=kernel_params['nu'] - ) - - params_array = np.array(state["params"]) - target_array = np.array(state["target"]) + params_array = np.asarray(state["params"], dtype=np.float64) + target_array = np.asarray(state["target"], dtype=np.float64) constraint_array = (np.array(state["constraint_values"]) if state["constraint_values"] is not None else None) @@ -449,6 +446,37 @@ def load_state(self, path: str | PathLike[str]) -> None: target=target, constraint_value=constraint ) - + self._acquisition_function.set_acquisition_params(state["acquisition_params"]) + + if state.get("transformed_bounds") and self._bounds_transformer: + new_bounds = { + key: bounds for key, bounds in zip( + self._space.keys, + np.array(state["transformed_bounds"]) + ) + } + self._space.set_bounds(new_bounds) + self._bounds_transformer.initialize(self._space) + + self._gp.set_params(**state["gp_params"]) + if isinstance(self._gp.kernel, dict): + kernel_params = self._gp.kernel + self._gp.kernel = Matern( + length_scale=kernel_params['length_scale'], + length_scale_bounds=tuple(kernel_params['length_scale_bounds']), + nu=kernel_params['nu'] + ) + self._gp.fit(self._space.params, self._space.target) + + if state["random_state"] is not None: + random_state_tuple = ( + state["random_state"]["bit_generator"], + np.array(state["random_state"]["state"], dtype=np.uint32), + state["random_state"]["pos"], + state["random_state"]["has_gauss"], + state["random_state"]["cached_gaussian"], + ) + self._random_state.set_state(random_state_tuple) + From 14ea11ac97834d949588a9c3d46651dd9206c017 Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Mon, 10 Feb 2025 13:54:40 +0000 Subject: [PATCH 12/31] move integration tests into acquisition --- tests/test_acquisition.py | 248 ++++++++++++++++++++++++++++++++++++- tests/test_integration.py | 250 -------------------------------------- 2 files changed, 247 insertions(+), 251 deletions(-) delete mode 100644 tests/test_integration.py diff --git a/tests/test_acquisition.py b/tests/test_acquisition.py index 12217c59a..5de421f55 100644 --- a/tests/test_acquisition.py +++ b/tests/test_acquisition.py @@ -5,12 +5,38 @@ import numpy as np import pytest from scipy.spatial.distance import pdist +from scipy.optimize import NonlinearConstraint from sklearn.gaussian_process import GaussianProcessRegressor -from bayes_opt import acquisition, exception +from bayes_opt import acquisition, exception, BayesianOptimization from bayes_opt.constraint import ConstraintModel from bayes_opt.target_space import TargetSpace +from bayes_opt.acquisition import ( + UpperConfidenceBound, + ProbabilityOfImprovement, + ExpectedImprovement, + ConstantLiar, + GPHedge +) + +# Test fixtures +@pytest.fixture +def target_func_x_and_y(): + return lambda x, y: -(x - 1)**2 - (y - 2)**2 + + +@pytest.fixture +def pbounds(): + return {"x": (-5, 5), "y": (-5, 5)} + +@pytest.fixture +def constraint(constraint_func): + return NonlinearConstraint( + fun=constraint_func, + lb=-1.0, + ub=4.0 + ) @pytest.fixture def target_func(): @@ -32,6 +58,11 @@ def target_space(target_func): return TargetSpace(target_func=target_func, pbounds={"x": (1, 4), "y": (0, 3.0)}) +@pytest.fixture +def constraint_func(): + return lambda x, y: x + y + + @pytest.fixture def constrained_target_space(target_func): constraint_model = ConstraintModel(fun=lambda params: params["x"] + params["y"], lb=0.0, ub=1.0) @@ -357,3 +388,218 @@ def test_gphedge_integration(gp, target_space, random_state): def test_upper_confidence_bound_invalid_kappa_error(kappa: float): with pytest.raises(ValueError, match="kappa must be greater than or equal to 0."): acquisition.UpperConfidenceBound(kappa=kappa) + + +def verify_optimizers_match(optimizer1, optimizer2): + """Helper function to verify two optimizers match.""" + assert len(optimizer1.space) == len(optimizer2.space) + assert optimizer1.max["target"] == optimizer2.max["target"] + assert optimizer1.max["params"] == optimizer2.max["params"] + + + + np.testing.assert_array_equal(optimizer1.space.params, optimizer2.space.params) + np.testing.assert_array_equal(optimizer1.space.target, optimizer2.space.target) + + if optimizer1.is_constrained: + np.testing.assert_array_equal( + optimizer1.space._constraint_values, + optimizer2.space._constraint_values + ) + assert optimizer1.space._constraint.lb == optimizer2.space._constraint.lb + assert optimizer1.space._constraint.ub == optimizer2.space._constraint.ub + + assert np.random.get_state()[1][0] == np.random.get_state()[1][0] + + assert optimizer1._gp.kernel.get_params() == optimizer2._gp.kernel.get_params() + + suggestion1 = optimizer1.suggest() + suggestion2 = optimizer2.suggest() + assert suggestion1 == suggestion2, f"\nSuggestion 1: {suggestion1}\nSuggestion 2: {suggestion2}" + + + + +def test_integration_upper_confidence_bound(target_func_x_and_y, pbounds, tmp_path): + """Test save/load integration with UpperConfidenceBound acquisition.""" + acquisition_function = UpperConfidenceBound(kappa=2.576) + + # Create and run first optimizer + optimizer = BayesianOptimization( + f=target_func_x_and_y, + pbounds=pbounds, + acquisition_function=acquisition_function, + random_state=1, + verbose=0 + ) + optimizer.maximize(init_points=2, n_iter=3) + + # Save state + state_path = tmp_path / "ucb_state.json" + optimizer.save_state(state_path) + + # Create new optimizer and load state + new_optimizer = BayesianOptimization( + f=target_func_x_and_y, + pbounds=pbounds, + acquisition_function=UpperConfidenceBound(kappa=2.576), + random_state=1, + verbose=0 + ) + new_optimizer.load_state(state_path) + + verify_optimizers_match(optimizer, new_optimizer) + +def test_integration_probability_improvement(target_func_x_and_y, pbounds, tmp_path): + """Test save/load integration with ProbabilityOfImprovement acquisition.""" + acquisition_function = ProbabilityOfImprovement(xi=0.01) + + optimizer = BayesianOptimization( + f=target_func_x_and_y, + pbounds=pbounds, + acquisition_function=acquisition_function, + random_state=1, + verbose=0 + ) + optimizer.maximize(init_points=2, n_iter=3) + + state_path = tmp_path / "pi_state.json" + optimizer.save_state(state_path) + + new_optimizer = BayesianOptimization( + f=target_func_x_and_y, + pbounds=pbounds, + acquisition_function=ProbabilityOfImprovement(xi=0.01), + random_state=1, + verbose=0 + ) + new_optimizer.load_state(state_path) + + verify_optimizers_match(optimizer, new_optimizer) + +def test_integration_expected_improvement(target_func_x_and_y, pbounds, tmp_path): + """Test save/load integration with ExpectedImprovement acquisition.""" + acquisition_function = ExpectedImprovement(xi=0.01) + + optimizer = BayesianOptimization( + f=target_func_x_and_y, + pbounds=pbounds, + acquisition_function=acquisition_function, + random_state=1, + verbose=0 + ) + optimizer.maximize(init_points=2, n_iter=3) + + state_path = tmp_path / "ei_state.json" + optimizer.save_state(state_path) + + new_optimizer = BayesianOptimization( + f=target_func_x_and_y, + pbounds=pbounds, + acquisition_function=ExpectedImprovement(xi=0.01), + random_state=1, + verbose=0 + ) + new_optimizer.load_state(state_path) + + verify_optimizers_match(optimizer, new_optimizer) + +def test_integration_constant_liar(target_func_x_and_y, pbounds, tmp_path): + """Test save/load integration with ConstantLiar acquisition.""" + base_acq = UpperConfidenceBound(kappa=2.576) + acquisition_function = ConstantLiar(base_acquisition=base_acq) + + optimizer = BayesianOptimization( + f=target_func_x_and_y, + pbounds=pbounds, + acquisition_function=acquisition_function, + random_state=1, + verbose=0 + ) + optimizer.maximize(init_points=2, n_iter=3) + + state_path = tmp_path / "cl_state.json" + optimizer.save_state(state_path) + + new_optimizer = BayesianOptimization( + f=target_func_x_and_y, + pbounds=pbounds, + acquisition_function=ConstantLiar(base_acquisition=UpperConfidenceBound(kappa=2.576)), + random_state=1, + verbose=0 + ) + new_optimizer.load_state(state_path) + + verify_optimizers_match(optimizer, new_optimizer) + +def test_integration_gp_hedge(target_func_x_and_y, pbounds, tmp_path): + """Test save/load integration with GPHedge acquisition.""" + base_acquisitions = [ + UpperConfidenceBound(kappa=2.576), + ProbabilityOfImprovement(xi=0.01), + ExpectedImprovement(xi=0.01) + ] + acquisition_function = GPHedge(base_acquisitions=base_acquisitions) + + optimizer = BayesianOptimization( + f=target_func_x_and_y, + pbounds=pbounds, + acquisition_function=acquisition_function, + random_state=1, + verbose=0 + ) + optimizer.maximize(init_points=2, n_iter=3) + + state_path = tmp_path / "gphedge_state.json" + optimizer.save_state(state_path) + + new_base_acquisitions = [ + UpperConfidenceBound(kappa=2.576), + ProbabilityOfImprovement(xi=0.01), + ExpectedImprovement(xi=0.01) + ] + new_optimizer = BayesianOptimization( + f=target_func_x_and_y, + pbounds=pbounds, + acquisition_function=GPHedge(base_acquisitions=new_base_acquisitions), + random_state=1, + verbose=0 + ) + new_optimizer.load_state(state_path) + + # Print new optimizer state + print("\nNew Optimizer State:") + print(f"GP random state: {new_optimizer._gp.random_state}") + print(f"GP kernel params:\n{new_optimizer._gp.kernel_.get_params()}") + print(f"Global random state: {np.random.get_state()[1][0]}") + + verify_optimizers_match(optimizer, new_optimizer) + +def test_integration_constrained(target_func_x_and_y, pbounds, constraint, tmp_path): + """Test save/load integration with constraints.""" + acquisition_function = ExpectedImprovement(xi=0.01) + + optimizer = BayesianOptimization( + f=target_func_x_and_y, + pbounds=pbounds, + acquisition_function=acquisition_function, + constraint=constraint, + random_state=1, + verbose=0 + ) + optimizer.maximize(init_points=2, n_iter=3) + + state_path = tmp_path / "constrained_state.json" + optimizer.save_state(state_path) + + new_optimizer = BayesianOptimization( + f=target_func_x_and_y, + pbounds=pbounds, + acquisition_function=ExpectedImprovement(xi=0.01), + constraint=constraint, + random_state=1, + verbose=0 + ) + new_optimizer.load_state(state_path) + + verify_optimizers_match(optimizer, new_optimizer) diff --git a/tests/test_integration.py b/tests/test_integration.py deleted file mode 100644 index 076995677..000000000 --- a/tests/test_integration.py +++ /dev/null @@ -1,250 +0,0 @@ -from __future__ import annotations - -import numpy as np -import pytest -from scipy.optimize import NonlinearConstraint -import json - -from bayes_opt import BayesianOptimization -from bayes_opt.acquisition import ( - UpperConfidenceBound, - ProbabilityOfImprovement, - ExpectedImprovement, - ConstantLiar, - GPHedge -) - -# Test fixtures -@pytest.fixture -def target_func(): - return lambda x, y: -(x - 1)**2 - (y - 2)**2 # Maximum at (1,2) - -@pytest.fixture -def pbounds(): - return {"x": (-5, 5), "y": (-5, 5)} - -@pytest.fixture -def constraint_func(): - return lambda x, y: x + y # Simple constraint: sum of parameters - -@pytest.fixture -def constraint(constraint_func): - return NonlinearConstraint( - fun=constraint_func, - lb=-1.0, - ub=4.0 - ) - -def verify_optimizers_match(optimizer1, optimizer2): - """Helper function to verify two optimizers match.""" - assert len(optimizer1.space) == len(optimizer2.space) - assert optimizer1.max["target"] == optimizer2.max["target"] - assert optimizer1.max["params"] == optimizer2.max["params"] - - - - np.testing.assert_array_equal(optimizer1.space.params, optimizer2.space.params) - np.testing.assert_array_equal(optimizer1.space.target, optimizer2.space.target) - - if optimizer1.is_constrained: - np.testing.assert_array_equal( - optimizer1.space._constraint_values, - optimizer2.space._constraint_values - ) - assert optimizer1.space._constraint.lb == optimizer2.space._constraint.lb - assert optimizer1.space._constraint.ub == optimizer2.space._constraint.ub - - assert np.random.get_state()[1][0] == np.random.get_state()[1][0] - - assert optimizer1._gp.kernel.get_params() == optimizer2._gp.kernel.get_params() - - suggestion1 = optimizer1.suggest() - suggestion2 = optimizer2.suggest() - assert suggestion1 == suggestion2, f"\nSuggestion 1: {suggestion1}\nSuggestion 2: {suggestion2}" - - - - -def test_integration_upper_confidence_bound(target_func, pbounds, tmp_path): - """Test save/load integration with UpperConfidenceBound acquisition.""" - acquisition_function = UpperConfidenceBound(kappa=2.576) - - # Create and run first optimizer - optimizer = BayesianOptimization( - f=target_func, - pbounds=pbounds, - acquisition_function=acquisition_function, - random_state=1, - verbose=0 - ) - optimizer.maximize(init_points=2, n_iter=3) - - # Save state - state_path = tmp_path / "ucb_state.json" - optimizer.save_state(state_path) - - # Create new optimizer and load state - new_optimizer = BayesianOptimization( - f=target_func, - pbounds=pbounds, - acquisition_function=UpperConfidenceBound(kappa=2.576), - random_state=1, - verbose=0 - ) - new_optimizer.load_state(state_path) - - verify_optimizers_match(optimizer, new_optimizer) - -def test_integration_probability_improvement(target_func, pbounds, tmp_path): - """Test save/load integration with ProbabilityOfImprovement acquisition.""" - acquisition_function = ProbabilityOfImprovement(xi=0.01) - - optimizer = BayesianOptimization( - f=target_func, - pbounds=pbounds, - acquisition_function=acquisition_function, - random_state=1, - verbose=0 - ) - optimizer.maximize(init_points=2, n_iter=3) - - state_path = tmp_path / "pi_state.json" - optimizer.save_state(state_path) - - new_optimizer = BayesianOptimization( - f=target_func, - pbounds=pbounds, - acquisition_function=ProbabilityOfImprovement(xi=0.01), - random_state=1, - verbose=0 - ) - new_optimizer.load_state(state_path) - - verify_optimizers_match(optimizer, new_optimizer) - -def test_integration_expected_improvement(target_func, pbounds, tmp_path): - """Test save/load integration with ExpectedImprovement acquisition.""" - acquisition_function = ExpectedImprovement(xi=0.01) - - optimizer = BayesianOptimization( - f=target_func, - pbounds=pbounds, - acquisition_function=acquisition_function, - random_state=1, - verbose=0 - ) - optimizer.maximize(init_points=2, n_iter=3) - - state_path = tmp_path / "ei_state.json" - optimizer.save_state(state_path) - - new_optimizer = BayesianOptimization( - f=target_func, - pbounds=pbounds, - acquisition_function=ExpectedImprovement(xi=0.01), - random_state=1, - verbose=0 - ) - new_optimizer.load_state(state_path) - - verify_optimizers_match(optimizer, new_optimizer) - -def test_integration_constant_liar(target_func, pbounds, tmp_path): - """Test save/load integration with ConstantLiar acquisition.""" - base_acq = UpperConfidenceBound(kappa=2.576) - acquisition_function = ConstantLiar(base_acquisition=base_acq) - - optimizer = BayesianOptimization( - f=target_func, - pbounds=pbounds, - acquisition_function=acquisition_function, - random_state=1, - verbose=0 - ) - optimizer.maximize(init_points=2, n_iter=3) - - state_path = tmp_path / "cl_state.json" - optimizer.save_state(state_path) - - new_optimizer = BayesianOptimization( - f=target_func, - pbounds=pbounds, - acquisition_function=ConstantLiar(base_acquisition=UpperConfidenceBound(kappa=2.576)), - random_state=1, - verbose=0 - ) - new_optimizer.load_state(state_path) - - verify_optimizers_match(optimizer, new_optimizer) - -def test_integration_gp_hedge(target_func, pbounds, tmp_path): - """Test save/load integration with GPHedge acquisition.""" - base_acquisitions = [ - UpperConfidenceBound(kappa=2.576), - ProbabilityOfImprovement(xi=0.01), - ExpectedImprovement(xi=0.01) - ] - acquisition_function = GPHedge(base_acquisitions=base_acquisitions) - - optimizer = BayesianOptimization( - f=target_func, - pbounds=pbounds, - acquisition_function=acquisition_function, - random_state=1, - verbose=0 - ) - optimizer.maximize(init_points=2, n_iter=3) - - state_path = tmp_path / "gphedge_state.json" - optimizer.save_state(state_path) - - new_base_acquisitions = [ - UpperConfidenceBound(kappa=2.576), - ProbabilityOfImprovement(xi=0.01), - ExpectedImprovement(xi=0.01) - ] - new_optimizer = BayesianOptimization( - f=target_func, - pbounds=pbounds, - acquisition_function=GPHedge(base_acquisitions=new_base_acquisitions), - random_state=1, - verbose=0 - ) - new_optimizer.load_state(state_path) - - # Print new optimizer state - print("\nNew Optimizer State:") - print(f"GP random state: {new_optimizer._gp.random_state}") - print(f"GP kernel params:\n{new_optimizer._gp.kernel_.get_params()}") - print(f"Global random state: {np.random.get_state()[1][0]}") - - verify_optimizers_match(optimizer, new_optimizer) - -def test_integration_constrained(target_func, pbounds, constraint, tmp_path): - """Test save/load integration with constraints.""" - acquisition_function = ExpectedImprovement(xi=0.01) - - optimizer = BayesianOptimization( - f=target_func, - pbounds=pbounds, - acquisition_function=acquisition_function, - constraint=constraint, - random_state=1, - verbose=0 - ) - optimizer.maximize(init_points=2, n_iter=3) - - state_path = tmp_path / "constrained_state.json" - optimizer.save_state(state_path) - - new_optimizer = BayesianOptimization( - f=target_func, - pbounds=pbounds, - acquisition_function=ExpectedImprovement(xi=0.01), - constraint=constraint, - random_state=1, - verbose=0 - ) - new_optimizer.load_state(state_path) - - verify_optimizers_match(optimizer, new_optimizer) From a7829607cddf6fc9463a62d14455c83d78ab4f40 Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Mon, 10 Feb 2025 13:54:58 +0000 Subject: [PATCH 13/31] remove unndecessary test, add tests for domain reduction and custom parameters --- tests/test_bayesian_optimization.py | 217 ++++++++++++++++++++++++---- 1 file changed, 187 insertions(+), 30 deletions(-) diff --git a/tests/test_bayesian_optimization.py b/tests/test_bayesian_optimization.py index 2831e40d3..6eb20728b 100644 --- a/tests/test_bayesian_optimization.py +++ b/tests/test_bayesian_optimization.py @@ -13,6 +13,9 @@ from bayes_opt.logger import ScreenLogger from bayes_opt.target_space import TargetSpace from scipy.optimize import NonlinearConstraint +from bayes_opt.domain_reduction import SequentialDomainReductionTransformer +from bayes_opt.parameter import BayesParameter +from bayes_opt.util import ensure_rng def target_func(**kwargs): @@ -415,20 +418,16 @@ def str_target_func(param1: str, param2: str) -> float: assert optimizer.res[i]["params"] == new_optimizer.res[i]["params"] -def test_probe_point_returns_same_point(tmp_path): - """Check that probe returns same point after save/load.""" +def test_suggest_point_returns_same_point(tmp_path): + """Check that suggest returns same point after save/load.""" optimizer = BayesianOptimization( f=target_func, pbounds=PBOUNDS, random_state=1, verbose=0 ) - - optimizer.register( - params={"p1": 5.0, "p2": 5.0}, - target=10.0 - ) - + optimizer.maximize(init_points=2, n_iter=3) + state_path = tmp_path / "optimizer_state.json" optimizer.save_state(state_path) @@ -439,27 +438,34 @@ def test_probe_point_returns_same_point(tmp_path): verbose=0 ) new_optimizer.load_state(state_path) - - # Both optimizers should probe the same point - point = {"p1": 1.5, "p2": 0.5} - probe1 = optimizer.probe(point) - probe2 = new_optimizer.probe(point) - assert probe1 == probe2 + + # Both optimizers should suggest the same point + suggestion1 = optimizer.suggest() + suggestion2 = new_optimizer.suggest() + assert suggestion1 == suggestion2 -def test_suggest_point_returns_same_point(tmp_path): - """Check that suggest returns same point after save/load.""" +def test_save_load_random_state(tmp_path): + """Test that random state is properly preserved.""" + # Initialize optimizer optimizer = BayesianOptimization( f=target_func, pbounds=PBOUNDS, random_state=1, verbose=0 ) - optimizer.maximize(init_points=2, n_iter=3) + # Register a point before saving + optimizer.probe( + params={"p1": 1, "p2": 2}, + lazy=False + ) + + # Save state state_path = tmp_path / "optimizer_state.json" optimizer.save_state(state_path) - + + # Create new optimizer with same configuration new_optimizer = BayesianOptimization( f=target_func, pbounds=PBOUNDS, @@ -474,9 +480,8 @@ def test_suggest_point_returns_same_point(tmp_path): assert suggestion1 == suggestion2 -def test_save_load_random_state(tmp_path): - """Test that random state is properly preserved.""" - # Initialize optimizer +def test_save_load_unused_optimizer(tmp_path): + """Test saving and loading optimizer state with unused optimizer.""" optimizer = BayesianOptimization( f=target_func, pbounds=PBOUNDS, @@ -484,25 +489,34 @@ def test_save_load_random_state(tmp_path): verbose=0 ) - # Save state - state_path = tmp_path / "optimizer_state.json" - optimizer.save_state(state_path) + # Test that saving without samples raises an error + with pytest.raises(ValueError, match="Cannot save optimizer state before collecting any samples"): + optimizer.save_state(tmp_path / "optimizer_state.json") + + # Add a sample point + optimizer.probe( + params={"p1": 1, "p2": 2}, + lazy=False + ) - # Load state and get next suggestion + # Now saving should work + optimizer.save_state(tmp_path / "optimizer_state.json") + new_optimizer = BayesianOptimization( f=target_func, pbounds=PBOUNDS, random_state=1, verbose=0 ) + new_optimizer.load_state(tmp_path / "optimizer_state.json") - # Both optimizers should suggest the same point - suggestion1 = optimizer.suggest() - suggestion2 = new_optimizer.suggest() - assert suggestion1 == suggestion2 + assert len(optimizer.space) == len(new_optimizer.space) + assert optimizer.max["target"] == new_optimizer.max["target"] + assert optimizer.max["params"] == new_optimizer.max["params"] + np.testing.assert_array_equal(optimizer.space.params, new_optimizer.space.params) + np.testing.assert_array_equal(optimizer.space.target, new_optimizer.space.target) -def test_save_load_w_constraint(tmp_path): """Test saving and loading optimizer state with constraints.""" def constraint_func(x: float, y: float) -> float: return x + y # Simple constraint: sum of parameters should be within bounds @@ -574,3 +588,146 @@ def constraint_func(x: float, y: float) -> float: assert 0.0 <= constraint_value <= 3.0, "Suggested point violates constraint" +def test_save_load_w_domain_reduction(tmp_path): + """Test saving and loading optimizer state with domain reduction transformer.""" + # Initialize optimizer with bounds transformer + bounds_transformer = SequentialDomainReductionTransformer() + optimizer = BayesianOptimization( + f=target_func, + pbounds=PBOUNDS, + random_state=1, + verbose=0, + bounds_transformer=bounds_transformer + ) + + # Run some iterations to trigger domain reduction + optimizer.maximize(init_points=2, n_iter=3) + + # Save state + state_path = tmp_path / "optimizer_state.json" + optimizer.save_state(state_path) + + # Create new optimizer with same configuration + new_bounds_transformer = SequentialDomainReductionTransformer() + new_optimizer = BayesianOptimization( + f=target_func, + pbounds=PBOUNDS, + random_state=1, + verbose=0, + bounds_transformer=new_bounds_transformer + ) + new_optimizer.load_state(state_path) + + # Both optimizers should probe the same point + point = {"p1": 1.5, "p2": 0.5} + probe1 = optimizer.probe(point) + probe2 = new_optimizer.probe(point) + assert probe1 == probe2 + + # Both optimizers should suggest the same point + suggestion1 = optimizer.suggest() + suggestion2 = new_optimizer.suggest() + assert suggestion1 == suggestion2 + + # Verify that the transformed bounds match + assert optimizer._space.bounds.tolist() == new_optimizer._space.bounds.tolist() + + +def test_save_load_w_custom_parameter(tmp_path): + """Test saving and loading optimizer state with custom parameter types.""" + + class FixedPerimeterTriangleParameter(BayesParameter): + def __init__(self, name: str, bounds, perimeter) -> None: + super().__init__(name, bounds) + self.perimeter = perimeter + + @property + def is_continuous(self): + return True + + def random_sample(self, n_samples: int, random_state): + random_state = ensure_rng(random_state) + samples = [] + while len(samples) < n_samples: + samples_ = random_state.dirichlet(np.ones(3), n_samples) + samples_ = samples_ * self.perimeter # scale samples by perimeter + + samples_ = samples_[np.all((self.bounds[:, 0] <= samples_) & (samples_ <= self.bounds[:, 1]), axis=-1)] + samples.extend(np.atleast_2d(samples_)) + samples = np.array(samples[:n_samples]) + return samples + + def to_float(self, value): + return value + + def to_param(self, value): + return value * self.perimeter / sum(value) + + def kernel_transform(self, value): + return value * self.perimeter / np.sum(value, axis=-1, keepdims=True) + + def to_string(self, value, str_len: int) -> str: + len_each = (str_len - 2) // 3 + str_ = '|'.join([f"{float(np.round(value[i], 4))}"[:len_each] for i in range(3)]) + return str_.ljust(str_len) + + @property + def dim(self): + return 3 # as we have three float values, each representing the length of one side. + + def area_of_triangle(sides): + a, b, c = sides + s = np.sum(sides, axis=-1) # perimeter + A = np.sqrt(s * (s-a) * (s-b) * (s-c)) + return A + + # Create parameter and bounds + param = FixedPerimeterTriangleParameter( + name='sides', + bounds=np.array([[0., 1.], [0., 1.], [0., 1.]]), + perimeter=1. + ) + pbounds = {'sides': param} + + # Print initial pbounds + print("\nOriginal pbounds:") + print(pbounds) + + # Initialize first optimizer + optimizer = BayesianOptimization( + f=area_of_triangle, + pbounds=pbounds, + random_state=1, + verbose=0 + ) + + # Run iterations and immediately save state + optimizer.maximize(init_points=2, n_iter=5) + + # Force GP update before saving + optimizer._gp.fit(optimizer.space.params, optimizer.space.target) + + # Save state + state_path = tmp_path / "optimizer_state.json" + optimizer.save_state(state_path) + + # Create new optimizer and load state + new_optimizer = BayesianOptimization( + f=area_of_triangle, + pbounds=pbounds, + random_state=1, + verbose=0 + ) + new_optimizer.load_state(state_path) + + suggestion1 = optimizer.suggest() + suggestion2 = new_optimizer.suggest() + + # Compare suggestions with reduced precision + np.testing.assert_array_almost_equal( + suggestion1['sides'], + suggestion2['sides'], + decimal=10 + ) + + From b209c648eda2bcb5c6a3e481f8320044f9e4d4fd Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Mon, 10 Feb 2025 13:57:45 +0000 Subject: [PATCH 14/31] make test more comprehensive --- tests/test_bayesian_optimization.py | 33 ++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/tests/test_bayesian_optimization.py b/tests/test_bayesian_optimization.py index 6eb20728b..24de16596 100644 --- a/tests/test_bayesian_optimization.py +++ b/tests/test_bayesian_optimization.py @@ -720,14 +720,31 @@ def area_of_triangle(sides): ) new_optimizer.load_state(state_path) - suggestion1 = optimizer.suggest() - suggestion2 = new_optimizer.suggest() + # Test that key properties match + assert len(optimizer.space) == len(new_optimizer.space) + assert optimizer.max["target"] == new_optimizer.max["target"] + assert optimizer.max["params"] == new_optimizer.max["params"] - # Compare suggestions with reduced precision - np.testing.assert_array_almost_equal( - suggestion1['sides'], - suggestion2['sides'], - decimal=10 - ) + # Test that all historical data matches + for i in range(len(optimizer.space)): + np.testing.assert_array_almost_equal( + optimizer.space.params[i], + new_optimizer.space.params[i] + ) + assert optimizer.space.target[i] == new_optimizer.space.target[i] + np.testing.assert_array_almost_equal( + optimizer.res[i]["params"]["sides"], + new_optimizer.res[i]["params"]["sides"] + ) + assert optimizer.res[i]["target"] == new_optimizer.res[i]["target"] + # Test that multiple subsequent suggestions match + for _ in range(5): + suggestion1 = optimizer.suggest() + suggestion2 = new_optimizer.suggest() + np.testing.assert_array_almost_equal( + suggestion1['sides'], + suggestion2['sides'], + decimal=10 + ) From 79b701c87331ef603f0e20f3006a1cc5f9cf40c0 Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Mon, 10 Feb 2025 13:57:59 +0000 Subject: [PATCH 15/31] add test logs --- examples/optimizer_state.json | 1391 +++++++++++++++++++++++++++++++++ 1 file changed, 1391 insertions(+) create mode 100644 examples/optimizer_state.json diff --git a/examples/optimizer_state.json b/examples/optimizer_state.json new file mode 100644 index 000000000..61ff864e6 --- /dev/null +++ b/examples/optimizer_state.json @@ -0,0 +1,1391 @@ +{ + "pbounds": { + "x": [ + -2.0, + 3.0 + ], + "y": [ + -3.0, + 3.0 + ] + }, + "transformed_bounds": null, + "keys": [ + "x", + "y" + ], + "params": [ + [ + 2.8340440094051482, + 1.3219469606529486 + ], + [ + 2.0002287496346898, + -1.1860045642089614 + ], + [ + 2.8375977943744273, + 1.3238498831039895 + ], + [ + 2.7487090390562576, + 1.2790562505410115 + ], + [ + 2.5885326650623566, + 1.2246876000015976 + ], + [ + 2.326944172045687, + 1.1533794918176512 + ], + [ + 1.8477442039231102, + 0.9230233313355451 + ], + [ + 1.0781673961198992, + 1.2152869055045181 + ], + [ + -0.29881203243864574, + 1.3619705214737496 + ], + [ + -0.6550608023832815, + 2.9904883318450866 + ], + [ + 0.5, + 0.7 + ], + [ + -0.3, + 0.1 + ], + [ + -1.9894076268088123, + 0.953633983654057 + ], + [ + 1.050970461249477, + 1.7803462461519244 + ], + [ + -1.9769335943399402, + -2.9275359019769653 + ], + [ + -0.22831222796273742, + 0.8046706513927941 + ], + [ + 0.20692536368024994, + 1.2101397649312364 + ] + ], + "target": [ + -7.135455292718879, + -7.779531005607566, + -7.156839989425082, + -6.633273772355583, + -5.750985875689304, + -4.438194448327629, + -2.420084050650126, + -0.20879338573767514, + 0.7796887108539075, + -3.391148454030464, + 0.6599999999999999, + 0.09999999999999998, + -2.959892513076863, + -0.7134791743023383, + -18.333804697747848, + 0.9097199721353757, + 0.913023173060441 + ], + "constraint_values": null, + "gp_params": { + "kernel": { + "length_scale": 1.0, + "length_scale_bounds": [ + 1e-05, + 100000.0 + ], + "nu": 2.5 + }, + "alpha": 1e-06, + "normalize_y": true, + "n_restarts_optimizer": 5 + }, + "allow_duplicate_points": false, + "verbose": 2, + "random_state": { + "bit_generator": "MT19937", + "state": [ + 3985086094, + 2093747867, + 4176436045, + 2777250632, + 1058317947, + 2919891432, + 387418789, + 560928139, + 3047456915, + 3345046077, + 73596132, + 3089956510, + 2517463479, + 1609952739, + 1588741219, + 4282325444, + 1342116766, + 1987295939, + 215985750, + 256961739, + 4021591681, + 2932417000, + 495113153, + 954472497, + 1089695600, + 2762468250, + 1718219156, + 770214364, + 3216929883, + 4247691151, + 1942402101, + 2017977502, + 3894964898, + 1591376795, + 2111118200, + 3721333322, + 1281927509, + 4135659726, + 26683426, + 2028912468, + 3585750030, + 1154530697, + 835528984, + 2926087410, + 3753407732, + 3053703758, + 1470891669, + 2093010326, + 2932988133, + 65196141, + 778628739, + 2942475932, + 1387563487, + 3755524055, + 2951653564, + 2251549857, + 2058872115, + 686913735, + 2731966551, + 2703017322, + 3042164256, + 3271795467, + 2840313430, + 2571972850, + 1332307421, + 617667670, + 3724223330, + 210310065, + 3163102685, + 4000350638, + 710624798, + 3167511256, + 2107592318, + 1470666316, + 3687145589, + 4132210945, + 1622455291, + 474835956, + 611027492, + 284403948, + 1327153417, + 2317756499, + 3301360800, + 4181239821, + 2416989259, + 1044179824, + 2460066028, + 3956180807, + 1332211589, + 4017074737, + 3922397919, + 1363068551, + 705079961, + 4288287482, + 961260361, + 1988885022, + 3274440427, + 1056914872, + 2968142141, + 2642839470, + 2734486142, + 3741357928, + 3665753515, + 567319486, + 4144814317, + 2533963088, + 194002109, + 3681422132, + 1356488824, + 1600521184, + 1715156865, + 2288467324, + 4038939394, + 3105238948, + 3049843993, + 1907457267, + 2368889637, + 4196551573, + 3015616896, + 2244488346, + 1309224261, + 220538342, + 1283886151, + 836318081, + 950150547, + 3501832980, + 677177370, + 1066222661, + 741265998, + 2329346898, + 4190575487, + 3041498374, + 653902688, + 390320204, + 1342154282, + 2339099517, + 1772718603, + 3861038875, + 2843134678, + 778108878, + 2493481016, + 1381607003, + 1494675708, + 1073258455, + 316279357, + 1255954945, + 1046634508, + 4089270960, + 4276089668, + 4168545385, + 3473865042, + 3339664749, + 3992945259, + 1740108363, + 2027642291, + 2022488320, + 682070207, + 2643631783, + 1096269231, + 2816795657, + 3348672752, + 4008088923, + 580113336, + 3508385721, + 3050500834, + 2853940538, + 2140854802, + 4243930213, + 2778314815, + 3105973779, + 1174494608, + 617928163, + 1336417160, + 3387836025, + 3858232024, + 2173231665, + 3740791484, + 2127823875, + 641026454, + 2696676224, + 859641306, + 2235644105, + 582015321, + 440306688, + 208020302, + 2424793205, + 2167405823, + 1026778062, + 3849915501, + 628779683, + 2856220826, + 3290502225, + 3364983777, + 3910259636, + 1106369170, + 1486301527, + 482326571, + 1748944762, + 2787556995, + 776674961, + 3026509740, + 110318482, + 2996504021, + 2845980272, + 2038987613, + 3133883528, + 3269372161, + 3408296256, + 1787690387, + 168970701, + 3368466593, + 2253280401, + 101976252, + 3446588383, + 2268896950, + 1692785728, + 3899694095, + 3294325971, + 3864199363, + 3089570789, + 833299996, + 3140804306, + 1567244650, + 2038463394, + 435978778, + 2234718620, + 208695965, + 2514388852, + 1619392886, + 3724215541, + 1435063239, + 2510183950, + 1123491971, + 2756540181, + 3493450288, + 2386256792, + 3117638468, + 3989206401, + 504122008, + 3109351205, + 1562662030, + 641852744, + 197829617, + 571561026, + 1501594552, + 1023545357, + 2040866707, + 459313856, + 2714504982, + 4225807326, + 3316310157, + 47670920, + 2270956728, + 1918427491, + 736410638, + 495105944, + 3242065542, + 2794875621, + 1548999156, + 2468341336, + 1672353177, + 757246295, + 397267540, + 2186228237, + 599666635, + 1912722195, + 312965632, + 1983538773, + 1736788952, + 2325890132, + 3618399441, + 3879561881, + 2377154974, + 3666651981, + 1454335242, + 1591972311, + 1822675021, + 734808613, + 4079924494, + 1060944543, + 4137102193, + 1878094849, + 751965056, + 1058457295, + 1725249091, + 381164945, + 3482460205, + 2355415388, + 8504863, + 3919314586, + 314337045, + 3503465290, + 134575590, + 775874508, + 2424978713, + 2189117498, + 4058780018, + 1871269053, + 3331335498, + 2352387380, + 190573384, + 762644601, + 3011761637, + 4206596717, + 319430881, + 1526266479, + 2117533715, + 2735890985, + 2075194867, + 3494840578, + 2889680993, + 1471786425, + 120324415, + 1720309176, + 3702652533, + 684878980, + 2523732434, + 3881300707, + 3904953271, + 4182326046, + 3631785995, + 20550752, + 363869154, + 4063445298, + 2825350589, + 1049472616, + 3302042570, + 830781876, + 1963520606, + 2259421601, + 2600763064, + 1996300191, + 3303537831, + 351407213, + 1877711896, + 734593942, + 958083114, + 209200478, + 3083253239, + 2540158965, + 3399801800, + 3129667731, + 2844219707, + 1310161331, + 3615766939, + 4043865684, + 753324257, + 3599201889, + 3188401761, + 3723876319, + 3273983567, + 3706197553, + 3505187083, + 1780191393, + 3361795491, + 4106652151, + 650012796, + 206507902, + 2814703005, + 3627202259, + 2111469660, + 3602090917, + 2166889278, + 2960736752, + 3074971825, + 1602132439, + 3037824193, + 1295205130, + 909563566, + 3745368630, + 2457717667, + 2341892836, + 3783489298, + 733985218, + 2491508054, + 2143297713, + 1767884825, + 318405692, + 3362683201, + 2701879069, + 2371024458, + 2540172147, + 337429029, + 3738269420, + 4257639854, + 4153889668, + 2375094728, + 1926786846, + 146087542, + 1098150316, + 3543665480, + 384045718, + 3950311573, + 1934212186, + 4055510837, + 1401450900, + 4058809460, + 3567951369, + 2925558499, + 2029893267, + 2992658704, + 2364611318, + 819059836, + 3018312442, + 3329518305, + 2676868776, + 209984705, + 1064327048, + 2452804490, + 3333961653, + 2886039023, + 3347126888, + 1866620022, + 3885764222, + 3738791207, + 2196819651, + 846773656, + 1642176185, + 2304713785, + 2245613694, + 830386505, + 3245071076, + 3598668732, + 1520862044, + 4055565226, + 3986829157, + 1565742470, + 1970968861, + 1118732185, + 3127264736, + 638777486, + 2932883771, + 3059256565, + 3798284923, + 1652959599, + 4284015612, + 1497924452, + 828186377, + 3586994666, + 46341785, + 2516984211, + 1815558050, + 655470013, + 3422807243, + 3541019294, + 2516197952, + 3216477793, + 1531696025, + 2726440021, + 1467632818, + 1783580678, + 1057388383, + 2317318404, + 3988832307, + 360636942, + 2135319019, + 3764950704, + 4188626816, + 2354931782, + 1221014098, + 524637247, + 2516659162, + 3368973572, + 2330208915, + 3869456990, + 1897789863, + 2476401916, + 3471915661, + 2423026352, + 1336039032, + 3709394520, + 49310030, + 599603168, + 706513014, + 842792151, + 3444457111, + 1146672514, + 4058302081, + 830475786, + 2813738921, + 2582097712, + 2443957537, + 473522987, + 2776551930, + 643767109, + 2158254323, + 1137986099, + 1828822412, + 2651508551, + 2637217784, + 629146168, + 4274583091, + 2431134014, + 2978879342, + 3640807315, + 940127825, + 1901903374, + 1269237331, + 2416192249, + 3341556405, + 2111359845, + 469424015, + 2664471964, + 3987606042, + 3175751522, + 3092103430, + 2317720038, + 2015983334, + 2495998928, + 992145747, + 3048366884, + 3555473434, + 3234424003, + 771413472, + 1005509592, + 1297920393, + 4103778064, + 1467578274, + 1599247527, + 965981955, + 360030456, + 265896422, + 557304704, + 764533039, + 1664614142, + 2005027675, + 2956138080, + 4275571687, + 3554440545, + 1977472534, + 50479426, + 3556138055, + 3461885865, + 1856712098, + 3056689282, + 3645681923, + 695164463, + 836477105, + 1040632049, + 3686180871, + 1650148888, + 1346301992, + 581013879, + 2149050036, + 4150069990, + 1033324477, + 1179267151, + 1256494775, + 3708377337, + 2804655043, + 2503823574, + 2259918197, + 3601674288, + 2568250507, + 2683513476, + 758429214, + 831418544, + 3171427624, + 3521462904, + 4093859849, + 2678198972, + 4019689119, + 522724641, + 3078042523, + 1301369926, + 760628792, + 270999696, + 734931530, + 2018510352, + 1691599835, + 1345325971, + 3674688525, + 616105615, + 844218209, + 3772876643, + 358611074, + 1461305730, + 2123603047, + 2423041389, + 3471214021, + 3108578035, + 3825404692, + 2701268156, + 1814767072, + 623218169, + 481562788, + 2054833579, + 1654783315, + 918704710, + 474369356, + 1588578260, + 1919828644, + 319299218, + 2583619890, + 1800129775, + 1543133403, + 2334155139, + 1298839443, + 1532269369, + 2690158390, + 1785713414, + 3129175501, + 894373214, + 4080824486, + 2227628748, + 3823269566, + 681495349, + 1263673268, + 4007769305, + 2505419614, + 3185455281, + 4053353561, + 3077406539, + 2834720641, + 2067341758, + 191495675, + 3390118678, + 2397024909, + 2919690034, + 1588903363, + 1668361658, + 548598153, + 3331808441, + 2268024627 + ], + "pos": 206, + "has_gauss": 0, + "cached_gaussian": 0.0 + }, + "acquisition_params": { + "kappa": 2.576, + "exploration_decay": null, + "exploration_decay_delay": null, + "random_state": { + "bit_generator": "MT19937", + "state": [ + 3985086094, + 2093747867, + 4176436045, + 2777250632, + 1058317947, + 2919891432, + 387418789, + 560928139, + 3047456915, + 3345046077, + 73596132, + 3089956510, + 2517463479, + 1609952739, + 1588741219, + 4282325444, + 1342116766, + 1987295939, + 215985750, + 256961739, + 4021591681, + 2932417000, + 495113153, + 954472497, + 1089695600, + 2762468250, + 1718219156, + 770214364, + 3216929883, + 4247691151, + 1942402101, + 2017977502, + 3894964898, + 1591376795, + 2111118200, + 3721333322, + 1281927509, + 4135659726, + 26683426, + 2028912468, + 3585750030, + 1154530697, + 835528984, + 2926087410, + 3753407732, + 3053703758, + 1470891669, + 2093010326, + 2932988133, + 65196141, + 778628739, + 2942475932, + 1387563487, + 3755524055, + 2951653564, + 2251549857, + 2058872115, + 686913735, + 2731966551, + 2703017322, + 3042164256, + 3271795467, + 2840313430, + 2571972850, + 1332307421, + 617667670, + 3724223330, + 210310065, + 3163102685, + 4000350638, + 710624798, + 3167511256, + 2107592318, + 1470666316, + 3687145589, + 4132210945, + 1622455291, + 474835956, + 611027492, + 284403948, + 1327153417, + 2317756499, + 3301360800, + 4181239821, + 2416989259, + 1044179824, + 2460066028, + 3956180807, + 1332211589, + 4017074737, + 3922397919, + 1363068551, + 705079961, + 4288287482, + 961260361, + 1988885022, + 3274440427, + 1056914872, + 2968142141, + 2642839470, + 2734486142, + 3741357928, + 3665753515, + 567319486, + 4144814317, + 2533963088, + 194002109, + 3681422132, + 1356488824, + 1600521184, + 1715156865, + 2288467324, + 4038939394, + 3105238948, + 3049843993, + 1907457267, + 2368889637, + 4196551573, + 3015616896, + 2244488346, + 1309224261, + 220538342, + 1283886151, + 836318081, + 950150547, + 3501832980, + 677177370, + 1066222661, + 741265998, + 2329346898, + 4190575487, + 3041498374, + 653902688, + 390320204, + 1342154282, + 2339099517, + 1772718603, + 3861038875, + 2843134678, + 778108878, + 2493481016, + 1381607003, + 1494675708, + 1073258455, + 316279357, + 1255954945, + 1046634508, + 4089270960, + 4276089668, + 4168545385, + 3473865042, + 3339664749, + 3992945259, + 1740108363, + 2027642291, + 2022488320, + 682070207, + 2643631783, + 1096269231, + 2816795657, + 3348672752, + 4008088923, + 580113336, + 3508385721, + 3050500834, + 2853940538, + 2140854802, + 4243930213, + 2778314815, + 3105973779, + 1174494608, + 617928163, + 1336417160, + 3387836025, + 3858232024, + 2173231665, + 3740791484, + 2127823875, + 641026454, + 2696676224, + 859641306, + 2235644105, + 582015321, + 440306688, + 208020302, + 2424793205, + 2167405823, + 1026778062, + 3849915501, + 628779683, + 2856220826, + 3290502225, + 3364983777, + 3910259636, + 1106369170, + 1486301527, + 482326571, + 1748944762, + 2787556995, + 776674961, + 3026509740, + 110318482, + 2996504021, + 2845980272, + 2038987613, + 3133883528, + 3269372161, + 3408296256, + 1787690387, + 168970701, + 3368466593, + 2253280401, + 101976252, + 3446588383, + 2268896950, + 1692785728, + 3899694095, + 3294325971, + 3864199363, + 3089570789, + 833299996, + 3140804306, + 1567244650, + 2038463394, + 435978778, + 2234718620, + 208695965, + 2514388852, + 1619392886, + 3724215541, + 1435063239, + 2510183950, + 1123491971, + 2756540181, + 3493450288, + 2386256792, + 3117638468, + 3989206401, + 504122008, + 3109351205, + 1562662030, + 641852744, + 197829617, + 571561026, + 1501594552, + 1023545357, + 2040866707, + 459313856, + 2714504982, + 4225807326, + 3316310157, + 47670920, + 2270956728, + 1918427491, + 736410638, + 495105944, + 3242065542, + 2794875621, + 1548999156, + 2468341336, + 1672353177, + 757246295, + 397267540, + 2186228237, + 599666635, + 1912722195, + 312965632, + 1983538773, + 1736788952, + 2325890132, + 3618399441, + 3879561881, + 2377154974, + 3666651981, + 1454335242, + 1591972311, + 1822675021, + 734808613, + 4079924494, + 1060944543, + 4137102193, + 1878094849, + 751965056, + 1058457295, + 1725249091, + 381164945, + 3482460205, + 2355415388, + 8504863, + 3919314586, + 314337045, + 3503465290, + 134575590, + 775874508, + 2424978713, + 2189117498, + 4058780018, + 1871269053, + 3331335498, + 2352387380, + 190573384, + 762644601, + 3011761637, + 4206596717, + 319430881, + 1526266479, + 2117533715, + 2735890985, + 2075194867, + 3494840578, + 2889680993, + 1471786425, + 120324415, + 1720309176, + 3702652533, + 684878980, + 2523732434, + 3881300707, + 3904953271, + 4182326046, + 3631785995, + 20550752, + 363869154, + 4063445298, + 2825350589, + 1049472616, + 3302042570, + 830781876, + 1963520606, + 2259421601, + 2600763064, + 1996300191, + 3303537831, + 351407213, + 1877711896, + 734593942, + 958083114, + 209200478, + 3083253239, + 2540158965, + 3399801800, + 3129667731, + 2844219707, + 1310161331, + 3615766939, + 4043865684, + 753324257, + 3599201889, + 3188401761, + 3723876319, + 3273983567, + 3706197553, + 3505187083, + 1780191393, + 3361795491, + 4106652151, + 650012796, + 206507902, + 2814703005, + 3627202259, + 2111469660, + 3602090917, + 2166889278, + 2960736752, + 3074971825, + 1602132439, + 3037824193, + 1295205130, + 909563566, + 3745368630, + 2457717667, + 2341892836, + 3783489298, + 733985218, + 2491508054, + 2143297713, + 1767884825, + 318405692, + 3362683201, + 2701879069, + 2371024458, + 2540172147, + 337429029, + 3738269420, + 4257639854, + 4153889668, + 2375094728, + 1926786846, + 146087542, + 1098150316, + 3543665480, + 384045718, + 3950311573, + 1934212186, + 4055510837, + 1401450900, + 4058809460, + 3567951369, + 2925558499, + 2029893267, + 2992658704, + 2364611318, + 819059836, + 3018312442, + 3329518305, + 2676868776, + 209984705, + 1064327048, + 2452804490, + 3333961653, + 2886039023, + 3347126888, + 1866620022, + 3885764222, + 3738791207, + 2196819651, + 846773656, + 1642176185, + 2304713785, + 2245613694, + 830386505, + 3245071076, + 3598668732, + 1520862044, + 4055565226, + 3986829157, + 1565742470, + 1970968861, + 1118732185, + 3127264736, + 638777486, + 2932883771, + 3059256565, + 3798284923, + 1652959599, + 4284015612, + 1497924452, + 828186377, + 3586994666, + 46341785, + 2516984211, + 1815558050, + 655470013, + 3422807243, + 3541019294, + 2516197952, + 3216477793, + 1531696025, + 2726440021, + 1467632818, + 1783580678, + 1057388383, + 2317318404, + 3988832307, + 360636942, + 2135319019, + 3764950704, + 4188626816, + 2354931782, + 1221014098, + 524637247, + 2516659162, + 3368973572, + 2330208915, + 3869456990, + 1897789863, + 2476401916, + 3471915661, + 2423026352, + 1336039032, + 3709394520, + 49310030, + 599603168, + 706513014, + 842792151, + 3444457111, + 1146672514, + 4058302081, + 830475786, + 2813738921, + 2582097712, + 2443957537, + 473522987, + 2776551930, + 643767109, + 2158254323, + 1137986099, + 1828822412, + 2651508551, + 2637217784, + 629146168, + 4274583091, + 2431134014, + 2978879342, + 3640807315, + 940127825, + 1901903374, + 1269237331, + 2416192249, + 3341556405, + 2111359845, + 469424015, + 2664471964, + 3987606042, + 3175751522, + 3092103430, + 2317720038, + 2015983334, + 2495998928, + 992145747, + 3048366884, + 3555473434, + 3234424003, + 771413472, + 1005509592, + 1297920393, + 4103778064, + 1467578274, + 1599247527, + 965981955, + 360030456, + 265896422, + 557304704, + 764533039, + 1664614142, + 2005027675, + 2956138080, + 4275571687, + 3554440545, + 1977472534, + 50479426, + 3556138055, + 3461885865, + 1856712098, + 3056689282, + 3645681923, + 695164463, + 836477105, + 1040632049, + 3686180871, + 1650148888, + 1346301992, + 581013879, + 2149050036, + 4150069990, + 1033324477, + 1179267151, + 1256494775, + 3708377337, + 2804655043, + 2503823574, + 2259918197, + 3601674288, + 2568250507, + 2683513476, + 758429214, + 831418544, + 3171427624, + 3521462904, + 4093859849, + 2678198972, + 4019689119, + 522724641, + 3078042523, + 1301369926, + 760628792, + 270999696, + 734931530, + 2018510352, + 1691599835, + 1345325971, + 3674688525, + 616105615, + 844218209, + 3772876643, + 358611074, + 1461305730, + 2123603047, + 2423041389, + 3471214021, + 3108578035, + 3825404692, + 2701268156, + 1814767072, + 623218169, + 481562788, + 2054833579, + 1654783315, + 918704710, + 474369356, + 1588578260, + 1919828644, + 319299218, + 2583619890, + 1800129775, + 1543133403, + 2334155139, + 1298839443, + 1532269369, + 2690158390, + 1785713414, + 3129175501, + 894373214, + 4080824486, + 2227628748, + 3823269566, + 681495349, + 1263673268, + 4007769305, + 2505419614, + 3185455281, + 4053353561, + 3077406539, + 2834720641, + 2067341758, + 191495675, + 3390118678, + 2397024909, + 2919690034, + 1588903363, + 1668361658, + 548598153, + 3331808441, + 2268024627 + ], + "pos": 206, + "has_gauss": 0, + "cached_gaussian": 0.0 + } + } +} \ No newline at end of file From 7d6b9d652f4d77fdb49cf50475d31018e0df392f Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Mon, 17 Feb 2025 09:07:31 +0100 Subject: [PATCH 16/31] sync execution counts from basic tour --- examples/basic-tour.ipynb | 68 +++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/examples/basic-tour.ipynb b/examples/basic-tour.ipynb index 9761deaff..aa5191dbf 100644 --- a/examples/basic-tour.ipynb +++ b/examples/basic-tour.ipynb @@ -28,7 +28,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -53,7 +53,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -62,7 +62,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -72,7 +72,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -97,7 +97,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -131,7 +131,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -155,7 +155,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -191,7 +191,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -200,7 +200,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -238,7 +238,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -257,7 +257,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -274,7 +274,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -286,7 +286,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -318,7 +318,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -341,7 +341,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -351,7 +351,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -387,7 +387,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 17, "metadata": {}, "outputs": [], "source": [ @@ -396,7 +396,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 18, "metadata": {}, "outputs": [ { @@ -410,7 +410,7 @@ "source": [ "new_optimizer = BayesianOptimization(\n", " f=black_box_function,\n", - " pbounds={\"x\": (-2, 2), \"y\": (-2, 2)},\n", + " pbounds={\"x\": (-3, 3), \"y\": (-3, 3)},\n", " verbose=2,\n", " random_state=7,\n", ")\n", @@ -419,7 +419,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 19, "metadata": {}, "outputs": [], "source": [ @@ -428,7 +428,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 20, "metadata": {}, "outputs": [ { @@ -445,7 +445,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 21, "metadata": {}, "outputs": [ { @@ -454,16 +454,16 @@ "text": [ "| iter | target | x | y |\n", "-------------------------------------------------\n", - "| \u001b[39m1 \u001b[39m | \u001b[39m-7.101 \u001b[39m | \u001b[39m1.9973177\u001b[39m | \u001b[39m-1.027773\u001b[39m |\n", - "| \u001b[39m2 \u001b[39m | \u001b[39m-0.6311 \u001b[39m | \u001b[39m-0.806373\u001b[39m | \u001b[39m1.9903823\u001b[39m |\n", - "| \u001b[39m3 \u001b[39m | \u001b[39m0.2572 \u001b[39m | \u001b[39m0.6921933\u001b[39m | \u001b[39m0.4865326\u001b[39m |\n", - "| \u001b[39m4 \u001b[39m | \u001b[39m-3.0 \u001b[39m | \u001b[39m1.9990210\u001b[39m | \u001b[39m0.9362987\u001b[39m |\n", - "| \u001b[39m5 \u001b[39m | \u001b[39m0.9011 \u001b[39m | \u001b[39m0.1556921\u001b[39m | \u001b[39m0.7268099\u001b[39m |\n", - "| \u001b[39m6 \u001b[39m | \u001b[39m0.9082 \u001b[39m | \u001b[39m-0.238045\u001b[39m | \u001b[39m1.1874540\u001b[39m |\n", - "| \u001b[35m7 \u001b[39m | \u001b[35m0.9988 \u001b[39m | \u001b[35m0.0204153\u001b[39m | \u001b[35m0.9723584\u001b[39m |\n", - "| \u001b[39m8 \u001b[39m | \u001b[39m0.8702 \u001b[39m | \u001b[39m0.3580753\u001b[39m | \u001b[39m0.9606191\u001b[39m |\n", - "| \u001b[39m9 \u001b[39m | \u001b[39m0.997 \u001b[39m | \u001b[39m-0.046534\u001b[39m | \u001b[39m1.0287398\u001b[39m |\n", - "| \u001b[39m10 \u001b[39m | \u001b[39m0.9979 \u001b[39m | \u001b[39m0.0295378\u001b[39m | \u001b[39m1.0346895\u001b[39m |\n", + "| \u001b[39m1 \u001b[39m | \u001b[39m-14.44 \u001b[39m | \u001b[39m2.9959766\u001b[39m | \u001b[39m-1.541659\u001b[39m |\n", + "| \u001b[39m2 \u001b[39m | \u001b[39m-3.938 \u001b[39m | \u001b[39m-0.992603\u001b[39m | \u001b[39m2.9881975\u001b[39m |\n", + "| \u001b[39m3 \u001b[39m | \u001b[39m-11.67 \u001b[39m | \u001b[39m2.9842190\u001b[39m | \u001b[39m2.9398042\u001b[39m |\n", + "| \u001b[39m4 \u001b[39m | \u001b[39m-11.43 \u001b[39m | \u001b[39m-2.966518\u001b[39m | \u001b[39m2.9062210\u001b[39m |\n", + "| \u001b[39m5 \u001b[39m | \u001b[39m0.3045 \u001b[39m | \u001b[39m-0.564519\u001b[39m | \u001b[39m1.6138208\u001b[39m |\n", + "| \u001b[39m6 \u001b[39m | \u001b[39m-3.176 \u001b[39m | \u001b[39m0.4898552\u001b[39m | \u001b[39m2.9838862\u001b[39m |\n", + "| \u001b[39m7 \u001b[39m | \u001b[39m0.05155 \u001b[39m | \u001b[39m0.7608462\u001b[39m | \u001b[39m0.3920796\u001b[39m |\n", + "| \u001b[39m8 \u001b[39m | \u001b[39m-0.2096 \u001b[39m | \u001b[39m-0.196874\u001b[39m | \u001b[39m-0.082066\u001b[39m |\n", + "| \u001b[39m9 \u001b[39m | \u001b[39m0.822 \u001b[39m | \u001b[39m0.2125014\u001b[39m | \u001b[39m0.6354894\u001b[39m |\n", + "| \u001b[39m10 \u001b[39m | \u001b[39m0.2598 \u001b[39m | \u001b[39m-0.769932\u001b[39m | \u001b[39m0.6160238\u001b[39m |\n", "=================================================\n" ] } @@ -496,7 +496,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 22, "metadata": {}, "outputs": [], "source": [ @@ -514,7 +514,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 23, "metadata": {}, "outputs": [], "source": [ From ab765b4d45709564683de809d7b72555d0a001f2 Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Wed, 5 Mar 2025 13:50:17 +0000 Subject: [PATCH 17/31] linting, whitespace removal, import structuring --- bayes_opt/acquisition.py | 151 ++++++++++----- bayes_opt/bayesian_optimization.py | 90 ++++----- examples/logs.log | 5 + tests/test_acquisition.py | 133 +++++++------- tests/test_bayesian_optimization.py | 276 +++++++++------------------- 5 files changed, 302 insertions(+), 353 deletions(-) create mode 100644 examples/logs.log diff --git a/bayes_opt/acquisition.py b/bayes_opt/acquisition.py index b60ca1b34..7d21239bc 100644 --- a/bayes_opt/acquisition.py +++ b/bayes_opt/acquisition.py @@ -74,11 +74,11 @@ def _serialize_random_state(self) -> dict | None: if self.random_state is not None: state = self.random_state.get_state() return { - 'bit_generator': state[0], - 'state': state[1].tolist(), # Convert numpy array to list - 'pos': state[2], - 'has_gauss': state[3], - 'cached_gaussian': state[4] + "bit_generator": state[0], + "state": state[1].tolist(), # Convert numpy array to list + "pos": state[2], + "has_gauss": state[3], + "cached_gaussian": state[4], } return None @@ -88,11 +88,11 @@ def _deserialize_random_state(self, state_dict: dict | None) -> None: if self.random_state is None: self.random_state = RandomState() state = ( - state_dict['bit_generator'], - np.array(state_dict['state'], dtype=np.uint32), - state_dict['pos'], - state_dict['has_gauss'], - state_dict['cached_gaussian'] + state_dict["bit_generator"], + np.array(state_dict["state"], dtype=np.uint32), + state_dict["pos"], + state_dict["has_gauss"], + state_dict["cached_gaussian"], ) self.random_state.set_state(state) @@ -102,7 +102,7 @@ def base_acq(self, *args: Any, **kwargs: Any) -> NDArray[Float]: def get_acquisition_params(self) -> dict[str, Any]: """Get the acquisition function parameters. - + Returns ------- dict @@ -110,16 +110,16 @@ def get_acquisition_params(self) -> dict[str, Any]: All values must be JSON serializable. """ return {} - - def set_acquisition_params(self, params: dict[str, Any]) -> None: + + def set_acquisition_params(self, params: dict) -> None: """Set the acquisition function parameters. - + Parameters ---------- params : dict Dictionary containing the acquisition function parameters. """ - pass + return {} def suggest( self, @@ -167,7 +167,7 @@ def suggest( acq = self._get_acq(gp=gp, constraint=target_space.constraint) return self._acq_min(acq, target_space, n_random=n_random, n_l_bfgs_b=n_l_bfgs_b) - + def _fit_gp(self, gp: GaussianProcessRegressor, target_space: TargetSpace) -> None: # Sklearn's GP throws a large number of warnings at times, but # we don't really need to see them here. @@ -501,16 +501,30 @@ def decay_exploration(self) -> None: self.exploration_decay_delay is None or self.exploration_decay_delay <= self.i ): self.kappa = self.kappa * self.exploration_decay - + def get_acquisition_params(self) -> dict: + """Get the current acquisition function parameters. + + Returns + ------- + dict + Dictionary containing the current acquisition function parameters. + """ return { "kappa": self.kappa, "exploration_decay": self.exploration_decay, "exploration_decay_delay": self.exploration_decay_delay, - "random_state": self._serialize_random_state() + "random_state": self._serialize_random_state(), } def set_acquisition_params(self, params: dict) -> None: + """Set the acquisition function parameters. + + Parameters + ---------- + params : dict + Dictionary containing the acquisition function parameters. + """ self.kappa = params["kappa"] self.exploration_decay = params["exploration_decay"] self.exploration_decay_delay = params["exploration_decay_delay"] @@ -648,17 +662,30 @@ def decay_exploration(self) -> None: self.exploration_decay_delay is None or self.exploration_decay_delay <= self.i ): self.xi = self.xi * self.exploration_decay - + def get_acquisition_params(self) -> dict: - """Get the acquisition function parameters.""" + """Get the current acquisition function parameters. + + Returns + ------- + dict + Dictionary containing the current acquisition function parameters. + """ return { "xi": self.xi, "exploration_decay": self.exploration_decay, "exploration_decay_delay": self.exploration_decay_delay, - "random_state": self._serialize_random_state() + "random_state": self._serialize_random_state(), } - + def set_acquisition_params(self, params: dict) -> None: + """Set the acquisition function parameters. + + Parameters + ---------- + params : dict + Dictionary containing the acquisition function parameters. + """ self.xi = params["xi"] self.exploration_decay = params["exploration_decay"] self.exploration_decay_delay = params["exploration_decay_delay"] @@ -804,16 +831,30 @@ def decay_exploration(self) -> None: self.exploration_decay_delay is None or self.exploration_decay_delay <= self.i ): self.xi = self.xi * self.exploration_decay - + def get_acquisition_params(self) -> dict: + """Get the current acquisition function parameters. + + Returns + ------- + dict + Dictionary containing the current acquisition function parameters. + """ return { "xi": self.xi, "exploration_decay": self.exploration_decay, "exploration_decay_delay": self.exploration_decay_delay, - "random_state": self._serialize_random_state() + "random_state": self._serialize_random_state(), } - + def set_acquisition_params(self, params: dict) -> None: + """Set the acquisition function parameters. + + Parameters + ---------- + params : dict + Dictionary containing the acquisition function parameters. + """ self.xi = params["xi"] self.exploration_decay = params["exploration_decay"] self.exploration_decay_delay = params["exploration_decay_delay"] @@ -1008,18 +1049,32 @@ def suggest( self.dummies.append(x_max) return x_max - + def get_acquisition_params(self) -> dict: + """Get the current acquisition function parameters. + + Returns + ------- + dict + Dictionary containing the current acquisition function parameters. + """ return { "dummies": [dummy.tolist() for dummy in self.dummies], "base_acquisition_params": self.base_acquisition.get_acquisition_params(), "strategy": self.strategy, "atol": self.atol, "rtol": self.rtol, - "random_state": self._serialize_random_state() + "random_state": self._serialize_random_state(), } - + def set_acquisition_params(self, params: dict) -> None: + """Set the acquisition function parameters. + + Parameters + ---------- + params : dict + Dictionary containing the acquisition function parameters. + """ self.dummies = [np.array(dummy) for dummy in params["dummies"]] self.base_acquisition.set_acquisition_params(params["base_acquisition_params"]) self.strategy = params["strategy"] @@ -1144,28 +1199,42 @@ def suggest( self.previous_candidates = np.array(x_max) idx = self._sample_idx_from_softmax_gains() return x_max[idx] - + def get_acquisition_params(self) -> dict: + """Get the current acquisition function parameters. + + Returns + ------- + dict + Dictionary containing the current acquisition function parameters. + """ return { "base_acquisitions_params": [acq.get_acquisition_params() for acq in self.base_acquisitions], "gains": self.gains.tolist(), - "previous_candidates": self.previous_candidates.tolist() if self.previous_candidates is not None else None, - "random_states": [acq._serialize_random_state() for acq in self.base_acquisitions] + [self._serialize_random_state()] + "previous_candidates": self.previous_candidates.tolist() + if self.previous_candidates is not None + else None, + "random_states": [acq._serialize_random_state() for acq in self.base_acquisitions] + + [self._serialize_random_state()], } - + def set_acquisition_params(self, params: dict) -> None: + """Set the acquisition function parameters. + + Parameters + ---------- + params : dict + Dictionary containing the acquisition function parameters. + """ for acq, acq_params, random_state in zip( - self.base_acquisitions, - params["base_acquisitions_params"], - params["random_states"][:-1] + self.base_acquisitions, params["base_acquisitions_params"], params["random_states"][:-1] ): acq.set_acquisition_params(acq_params) acq._deserialize_random_state(random_state) - + self.gains = np.array(params["gains"]) - self.previous_candidates = (np.array(params["previous_candidates"]) - if params["previous_candidates"] is not None - else None) - - self._deserialize_random_state(params["random_states"][-1]) + self.previous_candidates = ( + np.array(params["previous_candidates"]) if params["previous_candidates"] is not None else None + ) + self._deserialize_random_state(params["random_states"][-1]) diff --git a/bayes_opt/bayesian_optimization.py b/bayes_opt/bayesian_optimization.py index 17976721a..567030db3 100644 --- a/bayes_opt/bayesian_optimization.py +++ b/bayes_opt/bayesian_optimization.py @@ -6,20 +6,18 @@ from __future__ import annotations +import json from collections import deque +from os import PathLike +from pathlib import Path from typing import TYPE_CHECKING, Any from warnings import warn -import json -from pathlib import Path -from os import PathLike - import numpy as np +from scipy.optimize import NonlinearConstraint from sklearn.gaussian_process import GaussianProcessRegressor from sklearn.gaussian_process.kernels import Matern -from scipy.optimize import NonlinearConstraint - from bayes_opt import acquisition from bayes_opt.constraint import ConstraintModel from bayes_opt.domain_reduction import DomainTransformer @@ -364,12 +362,12 @@ def set_gp_params(self, **params: Any) -> None: def save_state(self, path: str | PathLike[str]) -> None: """Save complete state for reconstruction of the optimizer. - + Parameters ---------- path : str or PathLike Path to save the optimization state - + Raises ------ ValueError @@ -380,34 +378,25 @@ def save_state(self, path: str | PathLike[str]) -> None: "Cannot save optimizer state before collecting any samples. " "Please probe or register at least one point before saving." ) - + random_state = None if self._random_state is not None: state_tuple = self._random_state.get_state() random_state = { - 'bit_generator': state_tuple[0], - 'state': state_tuple[1].tolist(), - 'pos': state_tuple[2], - 'has_gauss': state_tuple[3], - 'cached_gaussian': state_tuple[4], + "bit_generator": state_tuple[0], + "state": state_tuple[1].tolist(), + "pos": state_tuple[2], + "has_gauss": state_tuple[3], + "cached_gaussian": state_tuple[4], } # Get constraint values if they exist - constraint_values = (self._space._constraint_values.tolist() - if self.is_constrained - else None) + constraint_values = self._space._constraint_values.tolist() if self.is_constrained else None acquisition_params = self._acquisition_function.get_acquisition_params() state = { - "pbounds": { - key: self._space._bounds[i].tolist() - for i, key in enumerate(self._space.keys) - }, + "pbounds": {key: self._space._bounds[i].tolist() for i, key in enumerate(self._space.keys)}, # Add current transformed bounds if using bounds transformer - "transformed_bounds": ( - self._space.bounds.tolist() - if self._bounds_transformer - else None - ), + "transformed_bounds": (self._space.bounds.tolist() if self._bounds_transformer else None), "keys": self._space.keys, "params": np.array(self._space.params).tolist(), "target": self._space.target.tolist(), @@ -423,53 +412,53 @@ def save_state(self, path: str | PathLike[str]) -> None: "random_state": random_state, "acquisition_params": acquisition_params, } - - with Path(path).open('w') as f: + + with Path(path).open("w") as f: json.dump(state, f, indent=2) - + def load_state(self, path: str | PathLike[str]) -> None: - with Path(path).open('r') as file: + """Load optimizer state from a JSON file. + + Parameters + ---------- + path : str or PathLike + Path to the JSON file containing the optimizer state. + """ + with Path(path).open("r") as file: state = json.load(file) params_array = np.asarray(state["params"], dtype=np.float64) target_array = np.asarray(state["target"], dtype=np.float64) - constraint_array = (np.array(state["constraint_values"]) - if state["constraint_values"] is not None - else None) + constraint_array = ( + np.array(state["constraint_values"]) if state["constraint_values"] is not None else None + ) for i in range(len(params_array)): params = self._space.array_to_params(params_array[i]) target = target_array[i] constraint = constraint_array[i] if constraint_array is not None else None - self.register( - params=params, - target=target, - constraint_value=constraint - ) - + self.register(params=params, target=target, constraint_value=constraint) + self._acquisition_function.set_acquisition_params(state["acquisition_params"]) - + if state.get("transformed_bounds") and self._bounds_transformer: new_bounds = { - key: bounds for key, bounds in zip( - self._space.keys, - np.array(state["transformed_bounds"]) - ) + key: bounds for key, bounds in zip(self._space.keys, np.array(state["transformed_bounds"])) } self._space.set_bounds(new_bounds) self._bounds_transformer.initialize(self._space) - + self._gp.set_params(**state["gp_params"]) if isinstance(self._gp.kernel, dict): kernel_params = self._gp.kernel self._gp.kernel = Matern( - length_scale=kernel_params['length_scale'], - length_scale_bounds=tuple(kernel_params['length_scale_bounds']), - nu=kernel_params['nu'] + length_scale=kernel_params["length_scale"], + length_scale_bounds=tuple(kernel_params["length_scale_bounds"]), + nu=kernel_params["nu"], ) - + self._gp.fit(self._space.params, self._space.target) - + if state["random_state"] is not None: random_state_tuple = ( state["random_state"]["bit_generator"], @@ -479,4 +468,3 @@ def load_state(self, path: str | PathLike[str]) -> None: state["random_state"]["cached_gaussian"], ) self._random_state.set_state(random_state_tuple) - diff --git a/examples/logs.log b/examples/logs.log new file mode 100644 index 000000000..e3d6f7bad --- /dev/null +++ b/examples/logs.log @@ -0,0 +1,5 @@ +{"target": -2.959892513076863, "params": {"x": -1.9894076268088123, "y": 0.953633983654057}, "datetime": {"datetime": "2025-03-05 13:49:34", "elapsed": 0.0, "delta": 0.0}} +{"target": -0.7134791743023383, "params": {"x": 1.050970461249477, "y": 1.7803462461519244}, "datetime": {"datetime": "2025-03-05 13:49:34", "elapsed": 0.001098, "delta": 0.001098}} +{"target": -18.333804697747848, "params": {"x": -1.9769335943399402, "y": -2.9275359019769653}, "datetime": {"datetime": "2025-03-05 13:49:34", "elapsed": 0.052831, "delta": 0.051733}} +{"target": 0.9097199721353757, "params": {"x": -0.22831222796273742, "y": 0.8046706513927941}, "datetime": {"datetime": "2025-03-05 13:49:34", "elapsed": 0.111687, "delta": 0.058856}} +{"target": 0.913023173060441, "params": {"x": 0.20692536368024994, "y": 1.2101397649312364}, "datetime": {"datetime": "2025-03-05 13:49:34", "elapsed": 0.157964, "delta": 0.046277}} diff --git a/tests/test_acquisition.py b/tests/test_acquisition.py index 5de421f55..072bfe52d 100644 --- a/tests/test_acquisition.py +++ b/tests/test_acquisition.py @@ -4,25 +4,26 @@ import numpy as np import pytest -from scipy.spatial.distance import pdist from scipy.optimize import NonlinearConstraint +from scipy.spatial.distance import pdist from sklearn.gaussian_process import GaussianProcessRegressor -from bayes_opt import acquisition, exception, BayesianOptimization -from bayes_opt.constraint import ConstraintModel -from bayes_opt.target_space import TargetSpace +from bayes_opt import BayesianOptimization, acquisition, exception from bayes_opt.acquisition import ( - UpperConfidenceBound, - ProbabilityOfImprovement, - ExpectedImprovement, ConstantLiar, - GPHedge + ExpectedImprovement, + GPHedge, + ProbabilityOfImprovement, + UpperConfidenceBound, ) +from bayes_opt.constraint import ConstraintModel +from bayes_opt.target_space import TargetSpace + # Test fixtures @pytest.fixture def target_func_x_and_y(): - return lambda x, y: -(x - 1)**2 - (y - 2)**2 + return lambda x, y: -((x - 1) ** 2) - (y - 2) ** 2 @pytest.fixture @@ -32,11 +33,8 @@ def pbounds(): @pytest.fixture def constraint(constraint_func): - return NonlinearConstraint( - fun=constraint_func, - lb=-1.0, - ub=4.0 - ) + return NonlinearConstraint(fun=constraint_func, lb=-1.0, ub=4.0) + @pytest.fixture def target_func(): @@ -395,211 +393,206 @@ def verify_optimizers_match(optimizer1, optimizer2): assert len(optimizer1.space) == len(optimizer2.space) assert optimizer1.max["target"] == optimizer2.max["target"] assert optimizer1.max["params"] == optimizer2.max["params"] - - - + np.testing.assert_array_equal(optimizer1.space.params, optimizer2.space.params) np.testing.assert_array_equal(optimizer1.space.target, optimizer2.space.target) - + if optimizer1.is_constrained: np.testing.assert_array_equal( - optimizer1.space._constraint_values, - optimizer2.space._constraint_values + optimizer1.space._constraint_values, optimizer2.space._constraint_values ) assert optimizer1.space._constraint.lb == optimizer2.space._constraint.lb assert optimizer1.space._constraint.ub == optimizer2.space._constraint.ub - - assert np.random.get_state()[1][0] == np.random.get_state()[1][0] - + + rng = np.random.default_rng() + assert rng.bit_generator.state['state']['state'] == rng.bit_generator.state['state']['state'] + assert optimizer1._gp.kernel.get_params() == optimizer2._gp.kernel.get_params() - + suggestion1 = optimizer1.suggest() suggestion2 = optimizer2.suggest() assert suggestion1 == suggestion2, f"\nSuggestion 1: {suggestion1}\nSuggestion 2: {suggestion2}" - - def test_integration_upper_confidence_bound(target_func_x_and_y, pbounds, tmp_path): """Test save/load integration with UpperConfidenceBound acquisition.""" acquisition_function = UpperConfidenceBound(kappa=2.576) - + # Create and run first optimizer optimizer = BayesianOptimization( f=target_func_x_and_y, pbounds=pbounds, acquisition_function=acquisition_function, random_state=1, - verbose=0 + verbose=0, ) optimizer.maximize(init_points=2, n_iter=3) - + # Save state state_path = tmp_path / "ucb_state.json" optimizer.save_state(state_path) - + # Create new optimizer and load state new_optimizer = BayesianOptimization( f=target_func_x_and_y, pbounds=pbounds, acquisition_function=UpperConfidenceBound(kappa=2.576), random_state=1, - verbose=0 + verbose=0, ) new_optimizer.load_state(state_path) - + verify_optimizers_match(optimizer, new_optimizer) + def test_integration_probability_improvement(target_func_x_and_y, pbounds, tmp_path): """Test save/load integration with ProbabilityOfImprovement acquisition.""" acquisition_function = ProbabilityOfImprovement(xi=0.01) - + optimizer = BayesianOptimization( f=target_func_x_and_y, pbounds=pbounds, acquisition_function=acquisition_function, random_state=1, - verbose=0 + verbose=0, ) optimizer.maximize(init_points=2, n_iter=3) - + state_path = tmp_path / "pi_state.json" optimizer.save_state(state_path) - + new_optimizer = BayesianOptimization( f=target_func_x_and_y, pbounds=pbounds, acquisition_function=ProbabilityOfImprovement(xi=0.01), random_state=1, - verbose=0 + verbose=0, ) new_optimizer.load_state(state_path) - + verify_optimizers_match(optimizer, new_optimizer) + def test_integration_expected_improvement(target_func_x_and_y, pbounds, tmp_path): """Test save/load integration with ExpectedImprovement acquisition.""" acquisition_function = ExpectedImprovement(xi=0.01) - + optimizer = BayesianOptimization( f=target_func_x_and_y, pbounds=pbounds, acquisition_function=acquisition_function, random_state=1, - verbose=0 + verbose=0, ) optimizer.maximize(init_points=2, n_iter=3) - + state_path = tmp_path / "ei_state.json" optimizer.save_state(state_path) - + new_optimizer = BayesianOptimization( f=target_func_x_and_y, pbounds=pbounds, acquisition_function=ExpectedImprovement(xi=0.01), random_state=1, - verbose=0 + verbose=0, ) new_optimizer.load_state(state_path) - + verify_optimizers_match(optimizer, new_optimizer) + def test_integration_constant_liar(target_func_x_and_y, pbounds, tmp_path): """Test save/load integration with ConstantLiar acquisition.""" base_acq = UpperConfidenceBound(kappa=2.576) acquisition_function = ConstantLiar(base_acquisition=base_acq) - + optimizer = BayesianOptimization( f=target_func_x_and_y, pbounds=pbounds, acquisition_function=acquisition_function, random_state=1, - verbose=0 + verbose=0, ) optimizer.maximize(init_points=2, n_iter=3) - + state_path = tmp_path / "cl_state.json" optimizer.save_state(state_path) - + new_optimizer = BayesianOptimization( f=target_func_x_and_y, pbounds=pbounds, acquisition_function=ConstantLiar(base_acquisition=UpperConfidenceBound(kappa=2.576)), random_state=1, - verbose=0 + verbose=0, ) new_optimizer.load_state(state_path) - + verify_optimizers_match(optimizer, new_optimizer) + def test_integration_gp_hedge(target_func_x_and_y, pbounds, tmp_path): """Test save/load integration with GPHedge acquisition.""" base_acquisitions = [ UpperConfidenceBound(kappa=2.576), ProbabilityOfImprovement(xi=0.01), - ExpectedImprovement(xi=0.01) + ExpectedImprovement(xi=0.01), ] acquisition_function = GPHedge(base_acquisitions=base_acquisitions) - + optimizer = BayesianOptimization( f=target_func_x_and_y, pbounds=pbounds, acquisition_function=acquisition_function, random_state=1, - verbose=0 + verbose=0, ) optimizer.maximize(init_points=2, n_iter=3) - + state_path = tmp_path / "gphedge_state.json" optimizer.save_state(state_path) - + new_base_acquisitions = [ UpperConfidenceBound(kappa=2.576), ProbabilityOfImprovement(xi=0.01), - ExpectedImprovement(xi=0.01) + ExpectedImprovement(xi=0.01), ] new_optimizer = BayesianOptimization( f=target_func_x_and_y, pbounds=pbounds, acquisition_function=GPHedge(base_acquisitions=new_base_acquisitions), random_state=1, - verbose=0 + verbose=0, ) new_optimizer.load_state(state_path) - - # Print new optimizer state - print("\nNew Optimizer State:") - print(f"GP random state: {new_optimizer._gp.random_state}") - print(f"GP kernel params:\n{new_optimizer._gp.kernel_.get_params()}") - print(f"Global random state: {np.random.get_state()[1][0]}") - + verify_optimizers_match(optimizer, new_optimizer) + def test_integration_constrained(target_func_x_and_y, pbounds, constraint, tmp_path): """Test save/load integration with constraints.""" acquisition_function = ExpectedImprovement(xi=0.01) - + optimizer = BayesianOptimization( f=target_func_x_and_y, pbounds=pbounds, acquisition_function=acquisition_function, constraint=constraint, random_state=1, - verbose=0 + verbose=0, ) optimizer.maximize(init_points=2, n_iter=3) - + state_path = tmp_path / "constrained_state.json" optimizer.save_state(state_path) - + new_optimizer = BayesianOptimization( f=target_func_x_and_y, pbounds=pbounds, acquisition_function=ExpectedImprovement(xi=0.01), constraint=constraint, random_state=1, - verbose=0 + verbose=0, ) new_optimizer.load_state(state_path) - + verify_optimizers_match(optimizer, new_optimizer) diff --git a/tests/test_bayesian_optimization.py b/tests/test_bayesian_optimization.py index 24de16596..abb8aa949 100644 --- a/tests/test_bayesian_optimization.py +++ b/tests/test_bayesian_optimization.py @@ -5,16 +5,16 @@ import numpy as np import pytest +from scipy.optimize import NonlinearConstraint from bayes_opt import BayesianOptimization, acquisition from bayes_opt.acquisition import AcquisitionFunction +from bayes_opt.domain_reduction import SequentialDomainReductionTransformer from bayes_opt.event import DEFAULT_EVENTS, Events from bayes_opt.exception import NotUniqueError from bayes_opt.logger import ScreenLogger -from bayes_opt.target_space import TargetSpace -from scipy.optimize import NonlinearConstraint -from bayes_opt.domain_reduction import SequentialDomainReductionTransformer from bayes_opt.parameter import BayesParameter +from bayes_opt.target_space import TargetSpace from bayes_opt.util import ensure_rng @@ -342,27 +342,17 @@ def test_duplicate_points(): def test_save_load_state(tmp_path): """Test saving and loading optimizer state.""" # Initialize and run original optimizer - optimizer = BayesianOptimization( - f=target_func, - pbounds=PBOUNDS, - random_state=1, - verbose=0 - ) + optimizer = BayesianOptimization(f=target_func, pbounds=PBOUNDS, random_state=1, verbose=0) optimizer.maximize(init_points=2, n_iter=3) - + # Save state state_path = tmp_path / "optimizer_state.json" optimizer.save_state(state_path) - + # Create new optimizer and load state - new_optimizer = BayesianOptimization( - f=target_func, - pbounds=PBOUNDS, - random_state=1, - verbose=0 - ) + new_optimizer = BayesianOptimization(f=target_func, pbounds=PBOUNDS, random_state=1, verbose=0) new_optimizer.load_state(state_path) - + # Test that key properties match assert len(optimizer.space) == len(new_optimizer.space) assert optimizer.max["target"] == new_optimizer.max["target"] @@ -373,40 +363,24 @@ def test_save_load_state(tmp_path): def test_save_load_w_categorical_params(tmp_path): """Test saving and loading optimizer state with categorical parameters.""" + def str_target_func(param1: str, param2: str) -> float: # Simple function that maps strings to numbers - value_map = { - "low": 1.0, - "medium": 2.0, - "high": 3.0 - } + value_map = {"low": 1.0, "medium": 2.0, "high": 3.0} return value_map[param1] + value_map[param2] - - str_pbounds = { - "param1": ["low", "medium", "high"], - "param2": ["low", "medium", "high"] - } - - optimizer = BayesianOptimization( - f=str_target_func, - pbounds=str_pbounds, - random_state=1, - verbose=0 - ) - + + str_pbounds = {"param1": ["low", "medium", "high"], "param2": ["low", "medium", "high"]} + + optimizer = BayesianOptimization(f=str_target_func, pbounds=str_pbounds, random_state=1, verbose=0) + optimizer.maximize(init_points=2, n_iter=3) - + state_path = tmp_path / "optimizer_state.json" optimizer.save_state(state_path) - - new_optimizer = BayesianOptimization( - f=str_target_func, - pbounds=str_pbounds, - random_state=1, - verbose=0 - ) + + new_optimizer = BayesianOptimization(f=str_target_func, pbounds=str_pbounds, random_state=1, verbose=0) new_optimizer.load_state(state_path) - + assert len(optimizer.space) == len(new_optimizer.space) assert optimizer.max["target"] == new_optimizer.max["target"] assert optimizer.max["params"] == new_optimizer.max["params"] @@ -420,25 +394,15 @@ def str_target_func(param1: str, param2: str) -> float: def test_suggest_point_returns_same_point(tmp_path): """Check that suggest returns same point after save/load.""" - optimizer = BayesianOptimization( - f=target_func, - pbounds=PBOUNDS, - random_state=1, - verbose=0 - ) + optimizer = BayesianOptimization(f=target_func, pbounds=PBOUNDS, random_state=1, verbose=0) optimizer.maximize(init_points=2, n_iter=3) - + state_path = tmp_path / "optimizer_state.json" optimizer.save_state(state_path) - new_optimizer = BayesianOptimization( - f=target_func, - pbounds=PBOUNDS, - random_state=1, - verbose=0 - ) + new_optimizer = BayesianOptimization(f=target_func, pbounds=PBOUNDS, random_state=1, verbose=0) new_optimizer.load_state(state_path) - + # Both optimizers should suggest the same point suggestion1 = optimizer.suggest() suggestion2 = new_optimizer.suggest() @@ -448,32 +412,19 @@ def test_suggest_point_returns_same_point(tmp_path): def test_save_load_random_state(tmp_path): """Test that random state is properly preserved.""" # Initialize optimizer - optimizer = BayesianOptimization( - f=target_func, - pbounds=PBOUNDS, - random_state=1, - verbose=0 - ) - + optimizer = BayesianOptimization(f=target_func, pbounds=PBOUNDS, random_state=1, verbose=0) + # Register a point before saving - optimizer.probe( - params={"p1": 1, "p2": 2}, - lazy=False - ) - + optimizer.probe(params={"p1": 1, "p2": 2}, lazy=False) + # Save state state_path = tmp_path / "optimizer_state.json" optimizer.save_state(state_path) - + # Create new optimizer with same configuration - new_optimizer = BayesianOptimization( - f=target_func, - pbounds=PBOUNDS, - random_state=1, - verbose=0 - ) + new_optimizer = BayesianOptimization(f=target_func, pbounds=PBOUNDS, random_state=1, verbose=0) new_optimizer.load_state(state_path) - + # Both optimizers should suggest the same point suggestion1 = optimizer.suggest() suggestion2 = new_optimizer.suggest() @@ -482,107 +433,79 @@ def test_save_load_random_state(tmp_path): def test_save_load_unused_optimizer(tmp_path): """Test saving and loading optimizer state with unused optimizer.""" - optimizer = BayesianOptimization( - f=target_func, - pbounds=PBOUNDS, - random_state=1, - verbose=0 - ) - + optimizer = BayesianOptimization(f=target_func, pbounds=PBOUNDS, random_state=1, verbose=0) + # Test that saving without samples raises an error with pytest.raises(ValueError, match="Cannot save optimizer state before collecting any samples"): optimizer.save_state(tmp_path / "optimizer_state.json") - + # Add a sample point - optimizer.probe( - params={"p1": 1, "p2": 2}, - lazy=False - ) - + optimizer.probe(params={"p1": 1, "p2": 2}, lazy=False) + # Now saving should work optimizer.save_state(tmp_path / "optimizer_state.json") - new_optimizer = BayesianOptimization( - f=target_func, - pbounds=PBOUNDS, - random_state=1, - verbose=0 - ) + new_optimizer = BayesianOptimization(f=target_func, pbounds=PBOUNDS, random_state=1, verbose=0) new_optimizer.load_state(tmp_path / "optimizer_state.json") - + assert len(optimizer.space) == len(new_optimizer.space) assert optimizer.max["target"] == new_optimizer.max["target"] assert optimizer.max["params"] == new_optimizer.max["params"] np.testing.assert_array_equal(optimizer.space.params, new_optimizer.space.params) np.testing.assert_array_equal(optimizer.space.target, new_optimizer.space.target) - """Test saving and loading optimizer state with constraints.""" + def constraint_func(x: float, y: float) -> float: return x + y # Simple constraint: sum of parameters should be within bounds - - constraint = NonlinearConstraint( - fun=constraint_func, - lb=0.0, - ub=3.0 - ) - + + constraint = NonlinearConstraint(fun=constraint_func, lb=0.0, ub=3.0) + # Initialize optimizer with constraint optimizer = BayesianOptimization( - f=target_func, - pbounds={"x": (-1, 3), "y": (0, 5)}, - constraint=constraint, - random_state=1, - verbose=0 + f=target_func, pbounds={"x": (-1, 3), "y": (0, 5)}, constraint=constraint, random_state=1, verbose=0 ) - + # Register some points, some that satisfy constraint and some that don't optimizer.register( params={"x": 1.0, "y": 1.0}, # Satisfies constraint: sum = 2.0 target=2.0, - constraint_value=2.0 + constraint_value=2.0, ) optimizer.register( params={"x": 2.0, "y": 2.0}, # Violates constraint: sum = 4.0 target=4.0, - constraint_value=4.0 + constraint_value=4.0, ) optimizer.register( params={"x": 0.5, "y": 0.5}, # Satisfies constraint: sum = 1.0 target=1.0, - constraint_value=1.0 + constraint_value=1.0, ) - + state_path = tmp_path / "optimizer_state.json" optimizer.save_state(state_path) - + new_optimizer = BayesianOptimization( - f=target_func, - pbounds={"x": (-1, 3), "y": (0, 5)}, - constraint=constraint, - random_state=1, - verbose=0 + f=target_func, pbounds={"x": (-1, 3), "y": (0, 5)}, constraint=constraint, random_state=1, verbose=0 ) new_optimizer.load_state(state_path) - + # Test that key properties match assert len(optimizer.space) == len(new_optimizer.space) assert optimizer.max["target"] == new_optimizer.max["target"] assert optimizer.max["params"] == new_optimizer.max["params"] np.testing.assert_array_equal(optimizer.space.params, new_optimizer.space.params) np.testing.assert_array_equal(optimizer.space.target, new_optimizer.space.target) - + # Test that constraint values were properly saved and loaded - np.testing.assert_array_equal( - optimizer.space._constraint_values, - new_optimizer.space._constraint_values - ) - + np.testing.assert_array_equal(optimizer.space._constraint_values, new_optimizer.space._constraint_values) + # Test that both optimizers suggest the same point (should respect constraints) suggestion1 = optimizer.suggest() suggestion2 = new_optimizer.suggest() assert suggestion1 == suggestion2 - + # Verify that suggested point satisfies constraint constraint_value = constraint_func(**suggestion1) assert 0.0 <= constraint_value <= 3.0, "Suggested point violates constraint" @@ -593,49 +516,41 @@ def test_save_load_w_domain_reduction(tmp_path): # Initialize optimizer with bounds transformer bounds_transformer = SequentialDomainReductionTransformer() optimizer = BayesianOptimization( - f=target_func, - pbounds=PBOUNDS, - random_state=1, - verbose=0, - bounds_transformer=bounds_transformer + f=target_func, pbounds=PBOUNDS, random_state=1, verbose=0, bounds_transformer=bounds_transformer ) - + # Run some iterations to trigger domain reduction optimizer.maximize(init_points=2, n_iter=3) - + # Save state state_path = tmp_path / "optimizer_state.json" optimizer.save_state(state_path) - + # Create new optimizer with same configuration new_bounds_transformer = SequentialDomainReductionTransformer() new_optimizer = BayesianOptimization( - f=target_func, - pbounds=PBOUNDS, - random_state=1, - verbose=0, - bounds_transformer=new_bounds_transformer + f=target_func, pbounds=PBOUNDS, random_state=1, verbose=0, bounds_transformer=new_bounds_transformer ) new_optimizer.load_state(state_path) - + # Both optimizers should probe the same point point = {"p1": 1.5, "p2": 0.5} probe1 = optimizer.probe(point) probe2 = new_optimizer.probe(point) assert probe1 == probe2 - + # Both optimizers should suggest the same point suggestion1 = optimizer.suggest() suggestion2 = new_optimizer.suggest() assert suggestion1 == suggestion2 - + # Verify that the transformed bounds match assert optimizer._space.bounds.tolist() == new_optimizer._space.bounds.tolist() def test_save_load_w_custom_parameter(tmp_path): """Test saving and loading optimizer state with custom parameter types.""" - + class FixedPerimeterTriangleParameter(BayesParameter): def __init__(self, name: str, bounds, perimeter) -> None: super().__init__(name, bounds) @@ -650,12 +565,13 @@ def random_sample(self, n_samples: int, random_state): samples = [] while len(samples) < n_samples: samples_ = random_state.dirichlet(np.ones(3), n_samples) - samples_ = samples_ * self.perimeter # scale samples by perimeter + samples_ = samples_ * self.perimeter # scale samples by perimeter - samples_ = samples_[np.all((self.bounds[:, 0] <= samples_) & (samples_ <= self.bounds[:, 1]), axis=-1)] + samples_ = samples_[ + np.all((self.bounds[:, 0] <= samples_) & (samples_ <= self.bounds[:, 1]), axis=-1) + ] samples.extend(np.atleast_2d(samples_)) - samples = np.array(samples[:n_samples]) - return samples + return np.array(samples[:n_samples]) def to_float(self, value): return value @@ -668,83 +584,61 @@ def kernel_transform(self, value): def to_string(self, value, str_len: int) -> str: len_each = (str_len - 2) // 3 - str_ = '|'.join([f"{float(np.round(value[i], 4))}"[:len_each] for i in range(3)]) + str_ = "|".join([f"{float(np.round(value[i], 4))}"[:len_each] for i in range(3)]) return str_.ljust(str_len) @property def dim(self): - return 3 # as we have three float values, each representing the length of one side. + return 3 # as we have three float values, each representing the length of one side. def area_of_triangle(sides): a, b, c = sides - s = np.sum(sides, axis=-1) # perimeter - A = np.sqrt(s * (s-a) * (s-b) * (s-c)) - return A + s = np.sum(sides, axis=-1) # perimeter + return np.sqrt(s * (s - a) * (s - b) * (s - c)) # Create parameter and bounds param = FixedPerimeterTriangleParameter( - name='sides', - bounds=np.array([[0., 1.], [0., 1.], [0., 1.]]), - perimeter=1. + name="sides", bounds=np.array([[0.0, 1.0], [0.0, 1.0], [0.0, 1.0]]), perimeter=1.0 ) - pbounds = {'sides': param} + pbounds = {"sides": param} # Print initial pbounds print("\nOriginal pbounds:") print(pbounds) - + # Initialize first optimizer - optimizer = BayesianOptimization( - f=area_of_triangle, - pbounds=pbounds, - random_state=1, - verbose=0 - ) + optimizer = BayesianOptimization(f=area_of_triangle, pbounds=pbounds, random_state=1, verbose=0) # Run iterations and immediately save state optimizer.maximize(init_points=2, n_iter=5) - + # Force GP update before saving optimizer._gp.fit(optimizer.space.params, optimizer.space.target) - + # Save state state_path = tmp_path / "optimizer_state.json" optimizer.save_state(state_path) - + # Create new optimizer and load state - new_optimizer = BayesianOptimization( - f=area_of_triangle, - pbounds=pbounds, - random_state=1, - verbose=0 - ) + new_optimizer = BayesianOptimization(f=area_of_triangle, pbounds=pbounds, random_state=1, verbose=0) new_optimizer.load_state(state_path) # Test that key properties match assert len(optimizer.space) == len(new_optimizer.space) assert optimizer.max["target"] == new_optimizer.max["target"] assert optimizer.max["params"] == new_optimizer.max["params"] - + # Test that all historical data matches for i in range(len(optimizer.space)): - np.testing.assert_array_almost_equal( - optimizer.space.params[i], - new_optimizer.space.params[i] - ) + np.testing.assert_array_almost_equal(optimizer.space.params[i], new_optimizer.space.params[i]) assert optimizer.space.target[i] == new_optimizer.space.target[i] np.testing.assert_array_almost_equal( - optimizer.res[i]["params"]["sides"], - new_optimizer.res[i]["params"]["sides"] + optimizer.res[i]["params"]["sides"], new_optimizer.res[i]["params"]["sides"] ) assert optimizer.res[i]["target"] == new_optimizer.res[i]["target"] # Test that multiple subsequent suggestions match for _ in range(5): - suggestion1 = optimizer.suggest() + suggestion1 = optimizer.suggest() suggestion2 = new_optimizer.suggest() - np.testing.assert_array_almost_equal( - suggestion1['sides'], - suggestion2['sides'], - decimal=10 - ) - + np.testing.assert_array_almost_equal(suggestion1["sides"], suggestion2["sides"], decimal=10) From c2ea551f79d2d9d33688fd33fdf3a1a3d2952ba6 Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Wed, 5 Mar 2025 13:54:45 +0000 Subject: [PATCH 18/31] ruff fix for string literal in error message --- bayes_opt/bayesian_optimization.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bayes_opt/bayesian_optimization.py b/bayes_opt/bayesian_optimization.py index 567030db3..9e9a4a875 100644 --- a/bayes_opt/bayesian_optimization.py +++ b/bayes_opt/bayesian_optimization.py @@ -374,10 +374,11 @@ def save_state(self, path: str | PathLike[str]) -> None: If attempting to save state before collecting any samples. """ if len(self._space) == 0: - raise ValueError( - "Cannot save optimizer state before collecting any samples. " - "Please probe or register at least one point before saving." - ) + msg = ( + "Cannot save optimizer state before collecting any samples. " + "Please probe or register at least one point before saving." + ) + raise ValueError(msg) random_state = None if self._random_state is not None: From c21c6c6195f3becac272c431021be856b995b978 Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Wed, 5 Mar 2025 14:02:43 +0000 Subject: [PATCH 19/31] fix ruff complaints --- bayes_opt/bayesian_optimization.py | 6 +++--- tests/test_acquisition.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bayes_opt/bayesian_optimization.py b/bayes_opt/bayesian_optimization.py index 9e9a4a875..90c22d647 100644 --- a/bayes_opt/bayesian_optimization.py +++ b/bayes_opt/bayesian_optimization.py @@ -375,9 +375,9 @@ def save_state(self, path: str | PathLike[str]) -> None: """ if len(self._space) == 0: msg = ( - "Cannot save optimizer state before collecting any samples. " - "Please probe or register at least one point before saving." - ) + "Cannot save optimizer state before collecting any samples. " + "Please probe or register at least one point before saving." + ) raise ValueError(msg) random_state = None diff --git a/tests/test_acquisition.py b/tests/test_acquisition.py index 072bfe52d..369483ba3 100644 --- a/tests/test_acquisition.py +++ b/tests/test_acquisition.py @@ -405,7 +405,7 @@ def verify_optimizers_match(optimizer1, optimizer2): assert optimizer1.space._constraint.ub == optimizer2.space._constraint.ub rng = np.random.default_rng() - assert rng.bit_generator.state['state']['state'] == rng.bit_generator.state['state']['state'] + assert rng.bit_generator.state["state"]["state"] == rng.bit_generator.state["state"]["state"] assert optimizer1._gp.kernel.get_params() == optimizer2._gp.kernel.get_params() From 57092d9daa94a54aa858061386ef827733420f5f Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Wed, 5 Mar 2025 14:27:43 +0000 Subject: [PATCH 20/31] make all side param comparisons almost equal to account for slight numpy differences --- tests/test_bayesian_optimization.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_bayesian_optimization.py b/tests/test_bayesian_optimization.py index abb8aa949..0ffe7bc7a 100644 --- a/tests/test_bayesian_optimization.py +++ b/tests/test_bayesian_optimization.py @@ -626,14 +626,16 @@ def area_of_triangle(sides): # Test that key properties match assert len(optimizer.space) == len(new_optimizer.space) assert optimizer.max["target"] == new_optimizer.max["target"] - assert optimizer.max["params"] == new_optimizer.max["params"] + np.testing.assert_array_almost_equal( + optimizer.max["params"]["sides"], new_optimizer.max["params"]["sides"], decimal=10 + ) # Test that all historical data matches for i in range(len(optimizer.space)): - np.testing.assert_array_almost_equal(optimizer.space.params[i], new_optimizer.space.params[i]) + np.testing.assert_array_almost_equal(optimizer.space.params[i], new_optimizer.space.params[i], decimal=10) assert optimizer.space.target[i] == new_optimizer.space.target[i] np.testing.assert_array_almost_equal( - optimizer.res[i]["params"]["sides"], new_optimizer.res[i]["params"]["sides"] + optimizer.res[i]["params"]["sides"], new_optimizer.res[i]["params"]["sides"], decimal=10 ) assert optimizer.res[i]["target"] == new_optimizer.res[i]["target"] From 9e57bffc7967dbdd8d92a6b6165bb67fcf5f12fb Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Wed, 5 Mar 2025 15:06:13 +0000 Subject: [PATCH 21/31] reformat array comparison check --- tests/test_bayesian_optimization.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_bayesian_optimization.py b/tests/test_bayesian_optimization.py index 0ffe7bc7a..236de2de7 100644 --- a/tests/test_bayesian_optimization.py +++ b/tests/test_bayesian_optimization.py @@ -632,7 +632,9 @@ def area_of_triangle(sides): # Test that all historical data matches for i in range(len(optimizer.space)): - np.testing.assert_array_almost_equal(optimizer.space.params[i], new_optimizer.space.params[i], decimal=10) + np.testing.assert_array_almost_equal( + optimizer.space.params[i], new_optimizer.space.params[i], decimal=10 + ) assert optimizer.space.target[i] == new_optimizer.space.target[i] np.testing.assert_array_almost_equal( optimizer.res[i]["params"]["sides"], new_optimizer.res[i]["params"]["sides"], decimal=10 From 289a0d5bac34d2b593ec4356c46dfe287f47d921 Mon Sep 17 00:00:00 2001 From: phi-friday Date: Fri, 28 Feb 2025 01:03:22 +0900 Subject: [PATCH 22/31] upgrade poetry2.0 & apply pep621 (#545) * chore: upgrade poetry2.0 & apply pep621 * fix: replace poetry action(not support 2.0) * fix: exclude one matrix * chore: split numpy deps * fix: numpy constraints * fix: install root * chore: use install-poetry --- .github/workflows/build_docs.yml | 2 ++ .github/workflows/format_and_lint.yml | 2 ++ .github/workflows/run_tests.yml | 19 ++++++++-- pyproject.toml | 52 ++++++++++++++++----------- 4 files changed, 53 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 43e7d988e..4227baf14 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -28,6 +28,8 @@ jobs: run: sudo apt-get install -y pandoc - name: Install Poetry uses: snok/install-poetry@v1 + with: + version: 'latest' - name: Install package and test dependencies run: | poetry install --with dev,nbtools diff --git a/.github/workflows/format_and_lint.yml b/.github/workflows/format_and_lint.yml index 7a870481a..a7fffe021 100644 --- a/.github/workflows/format_and_lint.yml +++ b/.github/workflows/format_and_lint.yml @@ -19,6 +19,8 @@ jobs: python-version: "3.9" - name: Install Poetry uses: snok/install-poetry@v1 + with: + version: 'latest' - name: Install dependencies run: | poetry install --with dev diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 8501d8cd6..1475feb72 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -19,6 +19,9 @@ jobs: matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] numpy-version: [">=1.25,<2", ">=2"] + exclude: + - python-version: "3.13" + numpy-version: ">=1.25,<2" # numpy<2 is not supported on Python 3.13 steps: - uses: actions/checkout@v3 @@ -26,12 +29,24 @@ jobs: uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v5 + with: + version: "latest" - name: Install Poetry uses: snok/install-poetry@v1 + with: + version: 'latest' - name: Install test dependencies run: | - poetry add "numpy${{ matrix.numpy-version }}" - poetry install --with dev,nbtools + poetry self add poetry-plugin-export + poetry export -f requirements.txt --with dev,nbtools --without-hashes --output requirements-dev.txt + echo "numpy${{ matrix.numpy-version }}" >> constraints.txt + uv pip compile requirements-dev.txt --output-file requirements.txt \ + --python-version ${{ matrix.python-version }} \ + --override constraints.txt + poetry run pip install -r requirements.txt + poetry install --only-root - name: Test with pytest run: | poetry run pytest --cov-report xml --cov=bayes_opt/ diff --git a/pyproject.toml b/pyproject.toml index 884c3ac48..a1ff1d7be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,24 +1,36 @@ -[tool.poetry] +[project] name = "bayesian-optimization" version = "3.0.0b1" description = "Bayesian Optimization package" -authors = ["Fernando Nogueira"] -license = "MIT" +authors = [{ name = "Fernando Nogueira", email = "fmfnogueira@gmail.com" }] +license = { file = "LICENSE" } readme = "README.md" -packages = [{include = "bayes_opt"}] - -[tool.poetry.dependencies] -python = "^3.9" -scikit-learn = "^1.0.0" -numpy = ">=1.25" -scipy = [ - {version = "^1.0.0", python = "<3.13"}, - {version = "^1.14.1", python = ">=3.13"} +requires-python = ">=3.9,<4.0" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "scikit-learn>=1.0.0,<2.0.0", + "numpy>=1.25; python_version<'3.13'", + "numpy>=2.1.3; python_version>='3.13'", + "scipy>=1.0.0,<2.0.0; python_version<'3.13'", + "scipy>=1.14.1,<2.0.0; python_version>='3.13'", + "colorama>=0.4.6,<1.0.0", ] -colorama = "^0.4.6" + +[tool.poetry] +requires-poetry = ">=2.0" +packages = [{ include = "bayes_opt" }] -[tool.poetry.group.dev] # for testing/developing +[tool.poetry.group.dev] # for testing/developing optional = true [tool.poetry.group.dev.dependencies] pytest = "^8.0.0" @@ -28,7 +40,7 @@ ruff = "0.6.6" pre-commit = "^3.7.1" -[tool.poetry.group.nbtools] # for running/converting notebooks +[tool.poetry.group.nbtools] # for running/converting notebooks optional = true [tool.poetry.group.nbtools.dependencies] nbformat = "^5.9.2" @@ -38,17 +50,17 @@ matplotlib = "^3.0" nbsphinx = "^0.9.4" sphinx-immaterial = "^0.12.0" sphinx = [ - {version = "^7.0.0", python = "<3.10"}, - {version = "^8.0.0", python = ">=3.10"} + { version = "^7.0.0", python = "<3.10" }, + { version = "^8.0.0", python = ">=3.10" }, ] sphinx-autodoc-typehints = [ - {version = "^2.3.0", python = "<3.10"}, - {version = "^2.4.0", python = ">=3.10"} + { version = "^2.3.0", python = "<3.10" }, + { version = "^2.4.0", python = ">=3.10" }, ] [build-system] -requires = ["poetry-core"] +requires = ["poetry-core>=2.0"] build-backend = "poetry.core.masonry.api" [tool.coverage.report] From d0ef58acd9cdde97ebc90c1edc2df55a6928e46b Mon Sep 17 00:00:00 2001 From: till-m <36440677+till-m@users.noreply.github.com> Date: Sun, 9 Mar 2025 08:55:22 +0100 Subject: [PATCH 23/31] Fix coverage report (#552) --- .github/workflows/run_tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 1475feb72..6ae4602ca 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -51,4 +51,6 @@ jobs: run: | poetry run pytest --cov-report xml --cov=bayes_opt/ - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} From a68d72733561878e8cc00342336e377a3f2fa28b Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Sun, 9 Mar 2025 16:17:45 +0000 Subject: [PATCH 24/31] remove unnecessary files, have acquisition baseclass functions raise errors --- .gitignore | 6 +- bayes_opt/acquisition.py | 45 +- examples/logs.log | 5 - examples/optimizer_state.json | 1391 --------------------------------- 4 files changed, 31 insertions(+), 1416 deletions(-) delete mode 100644 examples/logs.log delete mode 100644 examples/optimizer_state.json diff --git a/.gitignore b/.gitignore index 786d95c87..4b159c12f 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,8 @@ docsrc/*.ipynb docsrc/static/* docsrc/README.md -poetry.lock \ No newline at end of file +poetry.lock + +# Add log files and optimizer state files to gitignore +examples/logs.log +examples/optimizer_state.json diff --git a/bayes_opt/acquisition.py b/bayes_opt/acquisition.py index 7d21239bc..61066d3d2 100644 --- a/bayes_opt/acquisition.py +++ b/bayes_opt/acquisition.py @@ -100,26 +100,42 @@ def _deserialize_random_state(self, state_dict: dict | None) -> None: def base_acq(self, *args: Any, **kwargs: Any) -> NDArray[Float]: """Provide access to the base acquisition function.""" - def get_acquisition_params(self) -> dict[str, Any]: - """Get the acquisition function parameters. + def _fit_gp(self, gp: GaussianProcessRegressor, target_space: TargetSpace) -> None: + # Sklearn's GP throws a large number of warnings at times, but + # we don't really need to see them here. + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + gp.fit(target_space.params, target_space.target) + if target_space.constraint is not None: + target_space.constraint.fit(target_space.params, target_space._constraint_values) + + def get_acquisition_params(self): + """ + Get the parameters of the acquisition function. Returns ------- dict - Dictionary containing the acquisition function parameters. - All values must be JSON serializable. + The parameters of the acquisition function. """ - return {} + error_msg = ( + "Custom AcquisitionFunction subclasses must implement their own get_acquisition_params method." + ) + raise NotImplementedError(error_msg) - def set_acquisition_params(self, params: dict) -> None: - """Set the acquisition function parameters. + def set_acquisition_params(self, **params): + """ + Set the parameters of the acquisition function. Parameters ---------- - params : dict - Dictionary containing the acquisition function parameters. + **params : dict + The parameters of the acquisition function. """ - return {} + error_msg = ( + "Custom AcquisitionFunction subclasses must implement their own set_acquisition_params method." + ) + raise NotImplementedError(error_msg) def suggest( self, @@ -168,15 +184,6 @@ def suggest( acq = self._get_acq(gp=gp, constraint=target_space.constraint) return self._acq_min(acq, target_space, n_random=n_random, n_l_bfgs_b=n_l_bfgs_b) - def _fit_gp(self, gp: GaussianProcessRegressor, target_space: TargetSpace) -> None: - # Sklearn's GP throws a large number of warnings at times, but - # we don't really need to see them here. - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - gp.fit(target_space.params, target_space.target) - if target_space.constraint is not None: - target_space.constraint.fit(target_space.params, target_space._constraint_values) - def _get_acq( self, gp: GaussianProcessRegressor, constraint: ConstraintModel | None = None ) -> Callable[[NDArray[Float]], NDArray[Float]]: diff --git a/examples/logs.log b/examples/logs.log deleted file mode 100644 index e3d6f7bad..000000000 --- a/examples/logs.log +++ /dev/null @@ -1,5 +0,0 @@ -{"target": -2.959892513076863, "params": {"x": -1.9894076268088123, "y": 0.953633983654057}, "datetime": {"datetime": "2025-03-05 13:49:34", "elapsed": 0.0, "delta": 0.0}} -{"target": -0.7134791743023383, "params": {"x": 1.050970461249477, "y": 1.7803462461519244}, "datetime": {"datetime": "2025-03-05 13:49:34", "elapsed": 0.001098, "delta": 0.001098}} -{"target": -18.333804697747848, "params": {"x": -1.9769335943399402, "y": -2.9275359019769653}, "datetime": {"datetime": "2025-03-05 13:49:34", "elapsed": 0.052831, "delta": 0.051733}} -{"target": 0.9097199721353757, "params": {"x": -0.22831222796273742, "y": 0.8046706513927941}, "datetime": {"datetime": "2025-03-05 13:49:34", "elapsed": 0.111687, "delta": 0.058856}} -{"target": 0.913023173060441, "params": {"x": 0.20692536368024994, "y": 1.2101397649312364}, "datetime": {"datetime": "2025-03-05 13:49:34", "elapsed": 0.157964, "delta": 0.046277}} diff --git a/examples/optimizer_state.json b/examples/optimizer_state.json deleted file mode 100644 index 61ff864e6..000000000 --- a/examples/optimizer_state.json +++ /dev/null @@ -1,1391 +0,0 @@ -{ - "pbounds": { - "x": [ - -2.0, - 3.0 - ], - "y": [ - -3.0, - 3.0 - ] - }, - "transformed_bounds": null, - "keys": [ - "x", - "y" - ], - "params": [ - [ - 2.8340440094051482, - 1.3219469606529486 - ], - [ - 2.0002287496346898, - -1.1860045642089614 - ], - [ - 2.8375977943744273, - 1.3238498831039895 - ], - [ - 2.7487090390562576, - 1.2790562505410115 - ], - [ - 2.5885326650623566, - 1.2246876000015976 - ], - [ - 2.326944172045687, - 1.1533794918176512 - ], - [ - 1.8477442039231102, - 0.9230233313355451 - ], - [ - 1.0781673961198992, - 1.2152869055045181 - ], - [ - -0.29881203243864574, - 1.3619705214737496 - ], - [ - -0.6550608023832815, - 2.9904883318450866 - ], - [ - 0.5, - 0.7 - ], - [ - -0.3, - 0.1 - ], - [ - -1.9894076268088123, - 0.953633983654057 - ], - [ - 1.050970461249477, - 1.7803462461519244 - ], - [ - -1.9769335943399402, - -2.9275359019769653 - ], - [ - -0.22831222796273742, - 0.8046706513927941 - ], - [ - 0.20692536368024994, - 1.2101397649312364 - ] - ], - "target": [ - -7.135455292718879, - -7.779531005607566, - -7.156839989425082, - -6.633273772355583, - -5.750985875689304, - -4.438194448327629, - -2.420084050650126, - -0.20879338573767514, - 0.7796887108539075, - -3.391148454030464, - 0.6599999999999999, - 0.09999999999999998, - -2.959892513076863, - -0.7134791743023383, - -18.333804697747848, - 0.9097199721353757, - 0.913023173060441 - ], - "constraint_values": null, - "gp_params": { - "kernel": { - "length_scale": 1.0, - "length_scale_bounds": [ - 1e-05, - 100000.0 - ], - "nu": 2.5 - }, - "alpha": 1e-06, - "normalize_y": true, - "n_restarts_optimizer": 5 - }, - "allow_duplicate_points": false, - "verbose": 2, - "random_state": { - "bit_generator": "MT19937", - "state": [ - 3985086094, - 2093747867, - 4176436045, - 2777250632, - 1058317947, - 2919891432, - 387418789, - 560928139, - 3047456915, - 3345046077, - 73596132, - 3089956510, - 2517463479, - 1609952739, - 1588741219, - 4282325444, - 1342116766, - 1987295939, - 215985750, - 256961739, - 4021591681, - 2932417000, - 495113153, - 954472497, - 1089695600, - 2762468250, - 1718219156, - 770214364, - 3216929883, - 4247691151, - 1942402101, - 2017977502, - 3894964898, - 1591376795, - 2111118200, - 3721333322, - 1281927509, - 4135659726, - 26683426, - 2028912468, - 3585750030, - 1154530697, - 835528984, - 2926087410, - 3753407732, - 3053703758, - 1470891669, - 2093010326, - 2932988133, - 65196141, - 778628739, - 2942475932, - 1387563487, - 3755524055, - 2951653564, - 2251549857, - 2058872115, - 686913735, - 2731966551, - 2703017322, - 3042164256, - 3271795467, - 2840313430, - 2571972850, - 1332307421, - 617667670, - 3724223330, - 210310065, - 3163102685, - 4000350638, - 710624798, - 3167511256, - 2107592318, - 1470666316, - 3687145589, - 4132210945, - 1622455291, - 474835956, - 611027492, - 284403948, - 1327153417, - 2317756499, - 3301360800, - 4181239821, - 2416989259, - 1044179824, - 2460066028, - 3956180807, - 1332211589, - 4017074737, - 3922397919, - 1363068551, - 705079961, - 4288287482, - 961260361, - 1988885022, - 3274440427, - 1056914872, - 2968142141, - 2642839470, - 2734486142, - 3741357928, - 3665753515, - 567319486, - 4144814317, - 2533963088, - 194002109, - 3681422132, - 1356488824, - 1600521184, - 1715156865, - 2288467324, - 4038939394, - 3105238948, - 3049843993, - 1907457267, - 2368889637, - 4196551573, - 3015616896, - 2244488346, - 1309224261, - 220538342, - 1283886151, - 836318081, - 950150547, - 3501832980, - 677177370, - 1066222661, - 741265998, - 2329346898, - 4190575487, - 3041498374, - 653902688, - 390320204, - 1342154282, - 2339099517, - 1772718603, - 3861038875, - 2843134678, - 778108878, - 2493481016, - 1381607003, - 1494675708, - 1073258455, - 316279357, - 1255954945, - 1046634508, - 4089270960, - 4276089668, - 4168545385, - 3473865042, - 3339664749, - 3992945259, - 1740108363, - 2027642291, - 2022488320, - 682070207, - 2643631783, - 1096269231, - 2816795657, - 3348672752, - 4008088923, - 580113336, - 3508385721, - 3050500834, - 2853940538, - 2140854802, - 4243930213, - 2778314815, - 3105973779, - 1174494608, - 617928163, - 1336417160, - 3387836025, - 3858232024, - 2173231665, - 3740791484, - 2127823875, - 641026454, - 2696676224, - 859641306, - 2235644105, - 582015321, - 440306688, - 208020302, - 2424793205, - 2167405823, - 1026778062, - 3849915501, - 628779683, - 2856220826, - 3290502225, - 3364983777, - 3910259636, - 1106369170, - 1486301527, - 482326571, - 1748944762, - 2787556995, - 776674961, - 3026509740, - 110318482, - 2996504021, - 2845980272, - 2038987613, - 3133883528, - 3269372161, - 3408296256, - 1787690387, - 168970701, - 3368466593, - 2253280401, - 101976252, - 3446588383, - 2268896950, - 1692785728, - 3899694095, - 3294325971, - 3864199363, - 3089570789, - 833299996, - 3140804306, - 1567244650, - 2038463394, - 435978778, - 2234718620, - 208695965, - 2514388852, - 1619392886, - 3724215541, - 1435063239, - 2510183950, - 1123491971, - 2756540181, - 3493450288, - 2386256792, - 3117638468, - 3989206401, - 504122008, - 3109351205, - 1562662030, - 641852744, - 197829617, - 571561026, - 1501594552, - 1023545357, - 2040866707, - 459313856, - 2714504982, - 4225807326, - 3316310157, - 47670920, - 2270956728, - 1918427491, - 736410638, - 495105944, - 3242065542, - 2794875621, - 1548999156, - 2468341336, - 1672353177, - 757246295, - 397267540, - 2186228237, - 599666635, - 1912722195, - 312965632, - 1983538773, - 1736788952, - 2325890132, - 3618399441, - 3879561881, - 2377154974, - 3666651981, - 1454335242, - 1591972311, - 1822675021, - 734808613, - 4079924494, - 1060944543, - 4137102193, - 1878094849, - 751965056, - 1058457295, - 1725249091, - 381164945, - 3482460205, - 2355415388, - 8504863, - 3919314586, - 314337045, - 3503465290, - 134575590, - 775874508, - 2424978713, - 2189117498, - 4058780018, - 1871269053, - 3331335498, - 2352387380, - 190573384, - 762644601, - 3011761637, - 4206596717, - 319430881, - 1526266479, - 2117533715, - 2735890985, - 2075194867, - 3494840578, - 2889680993, - 1471786425, - 120324415, - 1720309176, - 3702652533, - 684878980, - 2523732434, - 3881300707, - 3904953271, - 4182326046, - 3631785995, - 20550752, - 363869154, - 4063445298, - 2825350589, - 1049472616, - 3302042570, - 830781876, - 1963520606, - 2259421601, - 2600763064, - 1996300191, - 3303537831, - 351407213, - 1877711896, - 734593942, - 958083114, - 209200478, - 3083253239, - 2540158965, - 3399801800, - 3129667731, - 2844219707, - 1310161331, - 3615766939, - 4043865684, - 753324257, - 3599201889, - 3188401761, - 3723876319, - 3273983567, - 3706197553, - 3505187083, - 1780191393, - 3361795491, - 4106652151, - 650012796, - 206507902, - 2814703005, - 3627202259, - 2111469660, - 3602090917, - 2166889278, - 2960736752, - 3074971825, - 1602132439, - 3037824193, - 1295205130, - 909563566, - 3745368630, - 2457717667, - 2341892836, - 3783489298, - 733985218, - 2491508054, - 2143297713, - 1767884825, - 318405692, - 3362683201, - 2701879069, - 2371024458, - 2540172147, - 337429029, - 3738269420, - 4257639854, - 4153889668, - 2375094728, - 1926786846, - 146087542, - 1098150316, - 3543665480, - 384045718, - 3950311573, - 1934212186, - 4055510837, - 1401450900, - 4058809460, - 3567951369, - 2925558499, - 2029893267, - 2992658704, - 2364611318, - 819059836, - 3018312442, - 3329518305, - 2676868776, - 209984705, - 1064327048, - 2452804490, - 3333961653, - 2886039023, - 3347126888, - 1866620022, - 3885764222, - 3738791207, - 2196819651, - 846773656, - 1642176185, - 2304713785, - 2245613694, - 830386505, - 3245071076, - 3598668732, - 1520862044, - 4055565226, - 3986829157, - 1565742470, - 1970968861, - 1118732185, - 3127264736, - 638777486, - 2932883771, - 3059256565, - 3798284923, - 1652959599, - 4284015612, - 1497924452, - 828186377, - 3586994666, - 46341785, - 2516984211, - 1815558050, - 655470013, - 3422807243, - 3541019294, - 2516197952, - 3216477793, - 1531696025, - 2726440021, - 1467632818, - 1783580678, - 1057388383, - 2317318404, - 3988832307, - 360636942, - 2135319019, - 3764950704, - 4188626816, - 2354931782, - 1221014098, - 524637247, - 2516659162, - 3368973572, - 2330208915, - 3869456990, - 1897789863, - 2476401916, - 3471915661, - 2423026352, - 1336039032, - 3709394520, - 49310030, - 599603168, - 706513014, - 842792151, - 3444457111, - 1146672514, - 4058302081, - 830475786, - 2813738921, - 2582097712, - 2443957537, - 473522987, - 2776551930, - 643767109, - 2158254323, - 1137986099, - 1828822412, - 2651508551, - 2637217784, - 629146168, - 4274583091, - 2431134014, - 2978879342, - 3640807315, - 940127825, - 1901903374, - 1269237331, - 2416192249, - 3341556405, - 2111359845, - 469424015, - 2664471964, - 3987606042, - 3175751522, - 3092103430, - 2317720038, - 2015983334, - 2495998928, - 992145747, - 3048366884, - 3555473434, - 3234424003, - 771413472, - 1005509592, - 1297920393, - 4103778064, - 1467578274, - 1599247527, - 965981955, - 360030456, - 265896422, - 557304704, - 764533039, - 1664614142, - 2005027675, - 2956138080, - 4275571687, - 3554440545, - 1977472534, - 50479426, - 3556138055, - 3461885865, - 1856712098, - 3056689282, - 3645681923, - 695164463, - 836477105, - 1040632049, - 3686180871, - 1650148888, - 1346301992, - 581013879, - 2149050036, - 4150069990, - 1033324477, - 1179267151, - 1256494775, - 3708377337, - 2804655043, - 2503823574, - 2259918197, - 3601674288, - 2568250507, - 2683513476, - 758429214, - 831418544, - 3171427624, - 3521462904, - 4093859849, - 2678198972, - 4019689119, - 522724641, - 3078042523, - 1301369926, - 760628792, - 270999696, - 734931530, - 2018510352, - 1691599835, - 1345325971, - 3674688525, - 616105615, - 844218209, - 3772876643, - 358611074, - 1461305730, - 2123603047, - 2423041389, - 3471214021, - 3108578035, - 3825404692, - 2701268156, - 1814767072, - 623218169, - 481562788, - 2054833579, - 1654783315, - 918704710, - 474369356, - 1588578260, - 1919828644, - 319299218, - 2583619890, - 1800129775, - 1543133403, - 2334155139, - 1298839443, - 1532269369, - 2690158390, - 1785713414, - 3129175501, - 894373214, - 4080824486, - 2227628748, - 3823269566, - 681495349, - 1263673268, - 4007769305, - 2505419614, - 3185455281, - 4053353561, - 3077406539, - 2834720641, - 2067341758, - 191495675, - 3390118678, - 2397024909, - 2919690034, - 1588903363, - 1668361658, - 548598153, - 3331808441, - 2268024627 - ], - "pos": 206, - "has_gauss": 0, - "cached_gaussian": 0.0 - }, - "acquisition_params": { - "kappa": 2.576, - "exploration_decay": null, - "exploration_decay_delay": null, - "random_state": { - "bit_generator": "MT19937", - "state": [ - 3985086094, - 2093747867, - 4176436045, - 2777250632, - 1058317947, - 2919891432, - 387418789, - 560928139, - 3047456915, - 3345046077, - 73596132, - 3089956510, - 2517463479, - 1609952739, - 1588741219, - 4282325444, - 1342116766, - 1987295939, - 215985750, - 256961739, - 4021591681, - 2932417000, - 495113153, - 954472497, - 1089695600, - 2762468250, - 1718219156, - 770214364, - 3216929883, - 4247691151, - 1942402101, - 2017977502, - 3894964898, - 1591376795, - 2111118200, - 3721333322, - 1281927509, - 4135659726, - 26683426, - 2028912468, - 3585750030, - 1154530697, - 835528984, - 2926087410, - 3753407732, - 3053703758, - 1470891669, - 2093010326, - 2932988133, - 65196141, - 778628739, - 2942475932, - 1387563487, - 3755524055, - 2951653564, - 2251549857, - 2058872115, - 686913735, - 2731966551, - 2703017322, - 3042164256, - 3271795467, - 2840313430, - 2571972850, - 1332307421, - 617667670, - 3724223330, - 210310065, - 3163102685, - 4000350638, - 710624798, - 3167511256, - 2107592318, - 1470666316, - 3687145589, - 4132210945, - 1622455291, - 474835956, - 611027492, - 284403948, - 1327153417, - 2317756499, - 3301360800, - 4181239821, - 2416989259, - 1044179824, - 2460066028, - 3956180807, - 1332211589, - 4017074737, - 3922397919, - 1363068551, - 705079961, - 4288287482, - 961260361, - 1988885022, - 3274440427, - 1056914872, - 2968142141, - 2642839470, - 2734486142, - 3741357928, - 3665753515, - 567319486, - 4144814317, - 2533963088, - 194002109, - 3681422132, - 1356488824, - 1600521184, - 1715156865, - 2288467324, - 4038939394, - 3105238948, - 3049843993, - 1907457267, - 2368889637, - 4196551573, - 3015616896, - 2244488346, - 1309224261, - 220538342, - 1283886151, - 836318081, - 950150547, - 3501832980, - 677177370, - 1066222661, - 741265998, - 2329346898, - 4190575487, - 3041498374, - 653902688, - 390320204, - 1342154282, - 2339099517, - 1772718603, - 3861038875, - 2843134678, - 778108878, - 2493481016, - 1381607003, - 1494675708, - 1073258455, - 316279357, - 1255954945, - 1046634508, - 4089270960, - 4276089668, - 4168545385, - 3473865042, - 3339664749, - 3992945259, - 1740108363, - 2027642291, - 2022488320, - 682070207, - 2643631783, - 1096269231, - 2816795657, - 3348672752, - 4008088923, - 580113336, - 3508385721, - 3050500834, - 2853940538, - 2140854802, - 4243930213, - 2778314815, - 3105973779, - 1174494608, - 617928163, - 1336417160, - 3387836025, - 3858232024, - 2173231665, - 3740791484, - 2127823875, - 641026454, - 2696676224, - 859641306, - 2235644105, - 582015321, - 440306688, - 208020302, - 2424793205, - 2167405823, - 1026778062, - 3849915501, - 628779683, - 2856220826, - 3290502225, - 3364983777, - 3910259636, - 1106369170, - 1486301527, - 482326571, - 1748944762, - 2787556995, - 776674961, - 3026509740, - 110318482, - 2996504021, - 2845980272, - 2038987613, - 3133883528, - 3269372161, - 3408296256, - 1787690387, - 168970701, - 3368466593, - 2253280401, - 101976252, - 3446588383, - 2268896950, - 1692785728, - 3899694095, - 3294325971, - 3864199363, - 3089570789, - 833299996, - 3140804306, - 1567244650, - 2038463394, - 435978778, - 2234718620, - 208695965, - 2514388852, - 1619392886, - 3724215541, - 1435063239, - 2510183950, - 1123491971, - 2756540181, - 3493450288, - 2386256792, - 3117638468, - 3989206401, - 504122008, - 3109351205, - 1562662030, - 641852744, - 197829617, - 571561026, - 1501594552, - 1023545357, - 2040866707, - 459313856, - 2714504982, - 4225807326, - 3316310157, - 47670920, - 2270956728, - 1918427491, - 736410638, - 495105944, - 3242065542, - 2794875621, - 1548999156, - 2468341336, - 1672353177, - 757246295, - 397267540, - 2186228237, - 599666635, - 1912722195, - 312965632, - 1983538773, - 1736788952, - 2325890132, - 3618399441, - 3879561881, - 2377154974, - 3666651981, - 1454335242, - 1591972311, - 1822675021, - 734808613, - 4079924494, - 1060944543, - 4137102193, - 1878094849, - 751965056, - 1058457295, - 1725249091, - 381164945, - 3482460205, - 2355415388, - 8504863, - 3919314586, - 314337045, - 3503465290, - 134575590, - 775874508, - 2424978713, - 2189117498, - 4058780018, - 1871269053, - 3331335498, - 2352387380, - 190573384, - 762644601, - 3011761637, - 4206596717, - 319430881, - 1526266479, - 2117533715, - 2735890985, - 2075194867, - 3494840578, - 2889680993, - 1471786425, - 120324415, - 1720309176, - 3702652533, - 684878980, - 2523732434, - 3881300707, - 3904953271, - 4182326046, - 3631785995, - 20550752, - 363869154, - 4063445298, - 2825350589, - 1049472616, - 3302042570, - 830781876, - 1963520606, - 2259421601, - 2600763064, - 1996300191, - 3303537831, - 351407213, - 1877711896, - 734593942, - 958083114, - 209200478, - 3083253239, - 2540158965, - 3399801800, - 3129667731, - 2844219707, - 1310161331, - 3615766939, - 4043865684, - 753324257, - 3599201889, - 3188401761, - 3723876319, - 3273983567, - 3706197553, - 3505187083, - 1780191393, - 3361795491, - 4106652151, - 650012796, - 206507902, - 2814703005, - 3627202259, - 2111469660, - 3602090917, - 2166889278, - 2960736752, - 3074971825, - 1602132439, - 3037824193, - 1295205130, - 909563566, - 3745368630, - 2457717667, - 2341892836, - 3783489298, - 733985218, - 2491508054, - 2143297713, - 1767884825, - 318405692, - 3362683201, - 2701879069, - 2371024458, - 2540172147, - 337429029, - 3738269420, - 4257639854, - 4153889668, - 2375094728, - 1926786846, - 146087542, - 1098150316, - 3543665480, - 384045718, - 3950311573, - 1934212186, - 4055510837, - 1401450900, - 4058809460, - 3567951369, - 2925558499, - 2029893267, - 2992658704, - 2364611318, - 819059836, - 3018312442, - 3329518305, - 2676868776, - 209984705, - 1064327048, - 2452804490, - 3333961653, - 2886039023, - 3347126888, - 1866620022, - 3885764222, - 3738791207, - 2196819651, - 846773656, - 1642176185, - 2304713785, - 2245613694, - 830386505, - 3245071076, - 3598668732, - 1520862044, - 4055565226, - 3986829157, - 1565742470, - 1970968861, - 1118732185, - 3127264736, - 638777486, - 2932883771, - 3059256565, - 3798284923, - 1652959599, - 4284015612, - 1497924452, - 828186377, - 3586994666, - 46341785, - 2516984211, - 1815558050, - 655470013, - 3422807243, - 3541019294, - 2516197952, - 3216477793, - 1531696025, - 2726440021, - 1467632818, - 1783580678, - 1057388383, - 2317318404, - 3988832307, - 360636942, - 2135319019, - 3764950704, - 4188626816, - 2354931782, - 1221014098, - 524637247, - 2516659162, - 3368973572, - 2330208915, - 3869456990, - 1897789863, - 2476401916, - 3471915661, - 2423026352, - 1336039032, - 3709394520, - 49310030, - 599603168, - 706513014, - 842792151, - 3444457111, - 1146672514, - 4058302081, - 830475786, - 2813738921, - 2582097712, - 2443957537, - 473522987, - 2776551930, - 643767109, - 2158254323, - 1137986099, - 1828822412, - 2651508551, - 2637217784, - 629146168, - 4274583091, - 2431134014, - 2978879342, - 3640807315, - 940127825, - 1901903374, - 1269237331, - 2416192249, - 3341556405, - 2111359845, - 469424015, - 2664471964, - 3987606042, - 3175751522, - 3092103430, - 2317720038, - 2015983334, - 2495998928, - 992145747, - 3048366884, - 3555473434, - 3234424003, - 771413472, - 1005509592, - 1297920393, - 4103778064, - 1467578274, - 1599247527, - 965981955, - 360030456, - 265896422, - 557304704, - 764533039, - 1664614142, - 2005027675, - 2956138080, - 4275571687, - 3554440545, - 1977472534, - 50479426, - 3556138055, - 3461885865, - 1856712098, - 3056689282, - 3645681923, - 695164463, - 836477105, - 1040632049, - 3686180871, - 1650148888, - 1346301992, - 581013879, - 2149050036, - 4150069990, - 1033324477, - 1179267151, - 1256494775, - 3708377337, - 2804655043, - 2503823574, - 2259918197, - 3601674288, - 2568250507, - 2683513476, - 758429214, - 831418544, - 3171427624, - 3521462904, - 4093859849, - 2678198972, - 4019689119, - 522724641, - 3078042523, - 1301369926, - 760628792, - 270999696, - 734931530, - 2018510352, - 1691599835, - 1345325971, - 3674688525, - 616105615, - 844218209, - 3772876643, - 358611074, - 1461305730, - 2123603047, - 2423041389, - 3471214021, - 3108578035, - 3825404692, - 2701268156, - 1814767072, - 623218169, - 481562788, - 2054833579, - 1654783315, - 918704710, - 474369356, - 1588578260, - 1919828644, - 319299218, - 2583619890, - 1800129775, - 1543133403, - 2334155139, - 1298839443, - 1532269369, - 2690158390, - 1785713414, - 3129175501, - 894373214, - 4080824486, - 2227628748, - 3823269566, - 681495349, - 1263673268, - 4007769305, - 2505419614, - 3185455281, - 4053353561, - 3077406539, - 2834720641, - 2067341758, - 191495675, - 3390118678, - 2397024909, - 2919690034, - 1588903363, - 1668361658, - 548598153, - 3331808441, - 2268024627 - ], - "pos": 206, - "has_gauss": 0, - "cached_gaussian": 0.0 - } - } -} \ No newline at end of file From 3d3e53819d94fc08a6bbc6f14c9706d2d6539375 Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Sun, 9 Mar 2025 16:38:30 +0000 Subject: [PATCH 25/31] remove duplicate acquisition functions random state --- bayes_opt/acquisition.py | 10 ++++------ tests/test_acquisition.py | 42 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/bayes_opt/acquisition.py b/bayes_opt/acquisition.py index 61066d3d2..289d46e3f 100644 --- a/bayes_opt/acquisition.py +++ b/bayes_opt/acquisition.py @@ -1221,8 +1221,7 @@ def get_acquisition_params(self) -> dict: "previous_candidates": self.previous_candidates.tolist() if self.previous_candidates is not None else None, - "random_states": [acq._serialize_random_state() for acq in self.base_acquisitions] - + [self._serialize_random_state()], + "gphedge_random_state": self._serialize_random_state(), } def set_acquisition_params(self, params: dict) -> None: @@ -1233,15 +1232,14 @@ def set_acquisition_params(self, params: dict) -> None: params : dict Dictionary containing the acquisition function parameters. """ - for acq, acq_params, random_state in zip( - self.base_acquisitions, params["base_acquisitions_params"], params["random_states"][:-1] + for acq, acq_params in zip( + self.base_acquisitions, params["base_acquisitions_params"] ): acq.set_acquisition_params(acq_params) - acq._deserialize_random_state(random_state) self.gains = np.array(params["gains"]) self.previous_candidates = ( np.array(params["previous_candidates"]) if params["previous_candidates"] is not None else None ) - self._deserialize_random_state(params["random_states"][-1]) + self._deserialize_random_state(params["gphedge_random_state"]) diff --git a/tests/test_acquisition.py b/tests/test_acquisition.py index 369483ba3..e3d3e51ca 100644 --- a/tests/test_acquisition.py +++ b/tests/test_acquisition.py @@ -596,3 +596,45 @@ def test_integration_constrained(target_func_x_and_y, pbounds, constraint, tmp_p new_optimizer.load_state(state_path) verify_optimizers_match(optimizer, new_optimizer) + + +def test_custom_acquisition_without_get_params(): + """Test that a custom acquisition function without get_acquisition_params raises NotImplementedError.""" + + class CustomAcqWithoutGetParams(acquisition.AcquisitionFunction): + def __init__(self, random_state=None): + super().__init__(random_state=random_state) + + def base_acq(self, mean, std): + return mean + std + + def set_acquisition_params(self, params): + pass + + acq = CustomAcqWithoutGetParams() + with pytest.raises( + NotImplementedError, + match="Custom AcquisitionFunction subclasses must implement their own get_acquisition_params method", + ): + acq.get_acquisition_params() + + +def test_custom_acquisition_without_set_params(): + """Test that a custom acquisition function without set_acquisition_params raises NotImplementedError.""" + + class CustomAcqWithoutSetParams(acquisition.AcquisitionFunction): + def __init__(self, random_state=None): + super().__init__(random_state=random_state) + + def base_acq(self, mean, std): + return mean + std + + def get_acquisition_params(self): + return {} + + acq = CustomAcqWithoutSetParams() + with pytest.raises( + NotImplementedError, + match="Custom AcquisitionFunction subclasses must implement their own set_acquisition_params method", + ): + acq.set_acquisition_params(params={}) From cf87c7bef466d16880f6b621e6866ffc6e192f7c Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Sun, 9 Mar 2025 16:38:43 +0000 Subject: [PATCH 26/31] ruff format --- bayes_opt/acquisition.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bayes_opt/acquisition.py b/bayes_opt/acquisition.py index 289d46e3f..dfc90b286 100644 --- a/bayes_opt/acquisition.py +++ b/bayes_opt/acquisition.py @@ -1232,9 +1232,7 @@ def set_acquisition_params(self, params: dict) -> None: params : dict Dictionary containing the acquisition function parameters. """ - for acq, acq_params in zip( - self.base_acquisitions, params["base_acquisitions_params"] - ): + for acq, acq_params in zip(self.base_acquisitions, params["base_acquisitions_params"]): acq.set_acquisition_params(acq_params) self.gains = np.array(params["gains"]) From b5ae882eac9a66a4f3872099811ec9eadb7a02ba Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Mon, 10 Mar 2025 11:15:36 +0000 Subject: [PATCH 27/31] add type hints for base acquisition get/set functions --- bayes_opt/acquisition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bayes_opt/acquisition.py b/bayes_opt/acquisition.py index dfc90b286..047bf696d 100644 --- a/bayes_opt/acquisition.py +++ b/bayes_opt/acquisition.py @@ -109,7 +109,7 @@ def _fit_gp(self, gp: GaussianProcessRegressor, target_space: TargetSpace) -> No if target_space.constraint is not None: target_space.constraint.fit(target_space.params, target_space._constraint_values) - def get_acquisition_params(self): + def get_acquisition_params(self) -> dict[str, Any] | NoReturn: """ Get the parameters of the acquisition function. @@ -123,7 +123,7 @@ def get_acquisition_params(self): ) raise NotImplementedError(error_msg) - def set_acquisition_params(self, **params): + def set_acquisition_params(self, **params) -> None | NoReturn: """ Set the parameters of the acquisition function. From 02d264348c94e600b62fa390ed10b813e68ec628 Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Mon, 10 Mar 2025 11:38:42 +0000 Subject: [PATCH 28/31] remove noreturn --- bayes_opt/acquisition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bayes_opt/acquisition.py b/bayes_opt/acquisition.py index 047bf696d..a81a6df13 100644 --- a/bayes_opt/acquisition.py +++ b/bayes_opt/acquisition.py @@ -109,7 +109,7 @@ def _fit_gp(self, gp: GaussianProcessRegressor, target_space: TargetSpace) -> No if target_space.constraint is not None: target_space.constraint.fit(target_space.params, target_space._constraint_values) - def get_acquisition_params(self) -> dict[str, Any] | NoReturn: + def get_acquisition_params(self) -> dict[str, Any]: """ Get the parameters of the acquisition function. @@ -123,7 +123,7 @@ def get_acquisition_params(self) -> dict[str, Any] | NoReturn: ) raise NotImplementedError(error_msg) - def set_acquisition_params(self, **params) -> None | NoReturn: + def set_acquisition_params(self, **params) -> None: """ Set the parameters of the acquisition function. From 2f8ab64de094bf5f17ef1368748e6bab097a6484 Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Mon, 10 Mar 2025 11:46:54 +0000 Subject: [PATCH 29/31] remove former saving functionality from notebooks --- examples/acquisition_functions.ipynb | 32 +++-- examples/basic-tour.ipynb | 178 +-------------------------- 2 files changed, 24 insertions(+), 186 deletions(-) diff --git a/examples/acquisition_functions.ipynb b/examples/acquisition_functions.ipynb index 53e693754..16a4ccf0b 100644 --- a/examples/acquisition_functions.ipynb +++ b/examples/acquisition_functions.ipynb @@ -54,14 +54,14 @@ " 'Gaussian Process and Utility Function After {} Steps'.format(steps),\n", " fontsize=30\n", " )\n", - " \n", - " gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1]) \n", + "\n", + " gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1])\n", " axis = plt.subplot(gs[0])\n", " acq = plt.subplot(gs[1])\n", - " \n", + "\n", " x_obs = np.array([[res[\"params\"][\"x\"]] for res in optimizer.res])\n", " y_obs = np.array([res[\"target\"] for res in optimizer.res])\n", - " \n", + "\n", " acquisition_function_._fit_gp(optimizer._gp, optimizer._space)\n", " mu, sigma = posterior(optimizer, x)\n", "\n", @@ -69,10 +69,10 @@ " axis.plot(x_obs.flatten(), y_obs, 'D', markersize=8, label=u'Observations', color='r')\n", " axis.plot(x, mu, '--', color='k', label='Prediction')\n", "\n", - " axis.fill(np.concatenate([x, x[::-1]]), \n", + " axis.fill(np.concatenate([x, x[::-1]]),\n", " np.concatenate([mu - 1.9600 * sigma, (mu + 1.9600 * sigma)[::-1]]),\n", " alpha=.6, fc='c', ec='None', label='95% confidence interval')\n", - " \n", + "\n", " axis.set_xlim((-2, 10))\n", " axis.set_ylim((None, None))\n", " axis.set_ylabel('f(x)', fontdict={'size':20})\n", @@ -82,13 +82,13 @@ " x = x.flatten()\n", "\n", " acq.plot(x, utility, label='Utility Function', color='purple')\n", - " acq.plot(x[np.argmax(utility)], np.max(utility), '*', markersize=15, \n", + " acq.plot(x[np.argmax(utility)], np.max(utility), '*', markersize=15,\n", " label=u'Next Best Guess', markerfacecolor='gold', markeredgecolor='k', markeredgewidth=1)\n", " acq.set_xlim((-2, 10))\n", " #acq.set_ylim((0, np.max(utility) + 0.5))\n", " acq.set_ylabel('Utility', fontdict={'size':20})\n", " acq.set_xlabel('x', fontdict={'size':20})\n", - " \n", + "\n", " axis.legend(loc=2, bbox_to_anchor=(1.01, 1), borderaxespad=0.)\n", " acq.legend(loc=2, bbox_to_anchor=(1.01, 1), borderaxespad=0.)\n", " return fig, fig.axes" @@ -110,7 +110,7 @@ "class GreedyAcquisition(acquisition.AcquisitionFunction):\n", " def __init__(self, random_state=None):\n", " super().__init__(random_state)\n", - " \n", + "\n", " def base_acq(self, mean, std):\n", " return mean # disregard std" ] @@ -357,11 +357,17 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], - "source": [] + "source": [ + "### Saving and loading with custom acquisition functions\n", + "\n", + "If you are using your own custom acquisition function, you will need to save and load the acquisition function state as well. Acquisition functions have a `get_acquisition_params` and `set_acquisition_params` method that can be used to save and load the acquisition function state. The get_acquisition_params method returns a dictionary containing the acquisition function parameters. The set_acquisition_params method takes a dictionary containing the acquisition function parameters and updates the acquisition function state.\n", + "\n", + "```python\n", + "acquisition_function.get_acquisition_params()\n", + "```" + ] }, { "cell_type": "code", diff --git a/examples/basic-tour.ipynb b/examples/basic-tour.ipynb index aa5191dbf..959782aeb 100644 --- a/examples/basic-tour.ipynb +++ b/examples/basic-tour.ipynb @@ -309,186 +309,18 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 4. Saving, loading and restarting\n", + "## 4. Saving and loading the optimizer\n", "\n", - "By default you can follow the progress of your optimization by setting `verbose>0` when instantiating the `BayesianOptimization` object. If you need more control over logging/alerting you will need to use an observer. For more information about observers checkout the advanced tour notebook. Here we will only see how to use the native `JSONLogger` object to save to and load progress from files.\n", + "The optimizer state can be saved to a file and loaded from a file. This is useful for continuing an optimization from a previous state, or for analyzing the optimization history without running the optimizer again.\n", "\n", - "### 4.1 Saving progress" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "from bayes_opt.logger import JSONLogger\n", - "from bayes_opt.event import Events" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The observer paradigm works by:\n", - "1. Instantiating an observer object.\n", - "2. Tying the observer object to a particular event fired by an optimizer.\n", - "\n", - "The `BayesianOptimization` object fires a number of internal events during optimization, in particular, every time it probes the function and obtains a new parameter-target combination it will fire an `Events.OPTIMIZATION_STEP` event, which our logger will listen to.\n", - "\n", - "**Caveat:** The logger will not look back at previously probed points." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [], - "source": [ - "logger = JSONLogger(path=\"./logs.log\")\n", - "optimizer.subscribe(Events.OPTIMIZATION_STEP, logger)" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "| iter | target | x | y |\n", - "-------------------------------------------------\n", - "| \u001b[39m13 \u001b[39m | \u001b[39m-2.96 \u001b[39m | \u001b[39m-1.989407\u001b[39m | \u001b[39m0.9536339\u001b[39m |\n", - "| \u001b[39m14 \u001b[39m | \u001b[39m-0.7135 \u001b[39m | \u001b[39m1.0509704\u001b[39m | \u001b[39m1.7803462\u001b[39m |\n", - "| \u001b[39m15 \u001b[39m | \u001b[39m-18.33 \u001b[39m | \u001b[39m-1.976933\u001b[39m | \u001b[39m-2.927535\u001b[39m |\n", - "| \u001b[35m16 \u001b[39m | \u001b[35m0.9097 \u001b[39m | \u001b[35m-0.228312\u001b[39m | \u001b[35m0.8046706\u001b[39m |\n", - "| \u001b[35m17 \u001b[39m | \u001b[35m0.913 \u001b[39m | \u001b[35m0.2069253\u001b[39m | \u001b[35m1.2101397\u001b[39m |\n", - "=================================================\n" - ] - } - ], - "source": [ - "optimizer.maximize(\n", - " init_points=2,\n", - " n_iter=3,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 4.2 Loading progress\n", - "\n", - "Naturally, if you stored progress you will be able to load that onto a new instance of `BayesianOptimization`. The easiest way to do it is by invoking the `load_logs` function, from the `util` submodule." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [], - "source": [ - "from bayes_opt.util import load_logs" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0\n" - ] - } - ], - "source": [ - "new_optimizer = BayesianOptimization(\n", - " f=black_box_function,\n", - " pbounds={\"x\": (-3, 3), \"y\": (-3, 3)},\n", - " verbose=2,\n", - " random_state=7,\n", - ")\n", - "print(len(new_optimizer.space))" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [], - "source": [ - "load_logs(new_optimizer, logs=[\"./logs.log\"]);" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "New optimizer is now aware of 5 points.\n" - ] - } - ], - "source": [ - "print(\"New optimizer is now aware of {} points.\".format(len(new_optimizer.space)))" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "| iter | target | x | y |\n", - "-------------------------------------------------\n", - "| \u001b[39m1 \u001b[39m | \u001b[39m-14.44 \u001b[39m | \u001b[39m2.9959766\u001b[39m | \u001b[39m-1.541659\u001b[39m |\n", - "| \u001b[39m2 \u001b[39m | \u001b[39m-3.938 \u001b[39m | \u001b[39m-0.992603\u001b[39m | \u001b[39m2.9881975\u001b[39m |\n", - "| \u001b[39m3 \u001b[39m | \u001b[39m-11.67 \u001b[39m | \u001b[39m2.9842190\u001b[39m | \u001b[39m2.9398042\u001b[39m |\n", - "| \u001b[39m4 \u001b[39m | \u001b[39m-11.43 \u001b[39m | \u001b[39m-2.966518\u001b[39m | \u001b[39m2.9062210\u001b[39m |\n", - "| \u001b[39m5 \u001b[39m | \u001b[39m0.3045 \u001b[39m | \u001b[39m-0.564519\u001b[39m | \u001b[39m1.6138208\u001b[39m |\n", - "| \u001b[39m6 \u001b[39m | \u001b[39m-3.176 \u001b[39m | \u001b[39m0.4898552\u001b[39m | \u001b[39m2.9838862\u001b[39m |\n", - "| \u001b[39m7 \u001b[39m | \u001b[39m0.05155 \u001b[39m | \u001b[39m0.7608462\u001b[39m | \u001b[39m0.3920796\u001b[39m |\n", - "| \u001b[39m8 \u001b[39m | \u001b[39m-0.2096 \u001b[39m | \u001b[39m-0.196874\u001b[39m | \u001b[39m-0.082066\u001b[39m |\n", - "| \u001b[39m9 \u001b[39m | \u001b[39m0.822 \u001b[39m | \u001b[39m0.2125014\u001b[39m | \u001b[39m0.6354894\u001b[39m |\n", - "| \u001b[39m10 \u001b[39m | \u001b[39m0.2598 \u001b[39m | \u001b[39m-0.769932\u001b[39m | \u001b[39m0.6160238\u001b[39m |\n", - "=================================================\n" - ] - } - ], - "source": [ - "new_optimizer.maximize(\n", - " init_points=0,\n", - " n_iter=10,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 5. Saving and loading the optimizer state\n", - "\n", - "The optimizer state can be saved to a file and loaded from a file. This is useful for continuing an optimization from a previous state, or for analyzing the optimization history without running the optimizer again." + "Note: if you are using your own custom acquisition function, you will need to save and load the acquisition function state as well. This is done by calling the `get_acquisition_params` and `set_acquisition_params` methods of the acquisition function. See the acquisition function documentation for more information." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 5.1 Saving the optimizer state\n", + "### 4.1 Saving the optimizer state\n", "\n", "The optimizer state can be saved to a file using the `save_state` method.\n", "optimizer.save_state(\"./optimizer_state.json\")" @@ -507,7 +339,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 5.2 Loading the optimizer state\n", + "## 4.2 Loading the optimizer state\n", "\n", "To load with a previously saved state, pass the path of your saved state file to the `load_state_path` parameter. Note that if you've changed the bounds of your parameters, you'll need to pass the updated bounds to the new optimizer.\n" ] From 413f46775d71539adfe91dcd3489f835fbdcf420 Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Mon, 10 Mar 2025 12:42:10 +0000 Subject: [PATCH 30/31] increase legibility of custom acquisition example --- examples/acquisition_functions.ipynb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/acquisition_functions.ipynb b/examples/acquisition_functions.ipynb index 16a4ccf0b..c5a00fd85 100644 --- a/examples/acquisition_functions.ipynb +++ b/examples/acquisition_functions.ipynb @@ -362,11 +362,9 @@ "source": [ "### Saving and loading with custom acquisition functions\n", "\n", - "If you are using your own custom acquisition function, you will need to save and load the acquisition function state as well. Acquisition functions have a `get_acquisition_params` and `set_acquisition_params` method that can be used to save and load the acquisition function state. The get_acquisition_params method returns a dictionary containing the acquisition function parameters. The set_acquisition_params method takes a dictionary containing the acquisition function parameters and updates the acquisition function state.\n", + "Default acquisition functions have a `get_acquisition_params` and `set_acquisition_params` method that is used to save and load the acquisition function state when the optimizer is being saved and loaded. When writing and using a custom acquisition function, those methods need to also be defined. In order to have perfect replicability, all parameters that the acquisition function depends on need to be saved and loaded. The get_acquisition_params method should return a dictionary containing all acquisition function parameters. The set_acquisition_params method should take that same dictionary containing the acquisition function parameters and update the respective parameters.\n", "\n", - "```python\n", - "acquisition_function.get_acquisition_params()\n", - "```" + "_serialize_random_state and _deserialize_random_state are provided as part of the base class and should be included in the dictionary returned by `get_acquisition_params`. See any of the default acquisition functions for reference." ] }, { From 472fd93eafd27e2581a42558aebf2cc5e9f309a1 Mon Sep 17 00:00:00 2001 From: Adrian Molzon Date: Mon, 10 Mar 2025 13:31:44 +0000 Subject: [PATCH 31/31] explicitly stating the optionality of the saving and loading in custom acq functions --- examples/acquisition_functions.ipynb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/acquisition_functions.ipynb b/examples/acquisition_functions.ipynb index c5a00fd85..b3a84684b 100644 --- a/examples/acquisition_functions.ipynb +++ b/examples/acquisition_functions.ipynb @@ -362,6 +362,8 @@ "source": [ "### Saving and loading with custom acquisition functions\n", "\n", + "If saving and loading is desired functionality, the acquisition function needs to have a `get_acquisition_params` and `set_acquisition_params` method.\n", + "\n", "Default acquisition functions have a `get_acquisition_params` and `set_acquisition_params` method that is used to save and load the acquisition function state when the optimizer is being saved and loaded. When writing and using a custom acquisition function, those methods need to also be defined. In order to have perfect replicability, all parameters that the acquisition function depends on need to be saved and loaded. The get_acquisition_params method should return a dictionary containing all acquisition function parameters. The set_acquisition_params method should take that same dictionary containing the acquisition function parameters and update the respective parameters.\n", "\n", "_serialize_random_state and _deserialize_random_state are provided as part of the base class and should be included in the dictionary returned by `get_acquisition_params`. See any of the default acquisition functions for reference."