Skip to content

Commit 484df61

Browse files
committed
add ScipyNelderMead and ScipyPowell algorithms
1 parent 97e5c11 commit 484df61

2 files changed

Lines changed: 512 additions & 0 deletions

File tree

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
"""Nelder-Mead optimizer from scipy.optimize."""
2+
3+
# copyright: hyperactive developers, MIT License (see LICENSE file)
4+
5+
import numpy as np
6+
7+
from hyperactive.opt._adapters import _BaseScipyAdapter
8+
9+
__all__ = ["ScipyNelderMead"]
10+
11+
12+
class ScipyNelderMead(_BaseScipyAdapter):
13+
"""Scipy Nelder-Mead simplex optimizer.
14+
15+
Nelder-Mead is a derivative-free local optimization algorithm that uses
16+
a simplex to explore the search space. It is effective for:
17+
18+
* Local optimization and fine-tuning
19+
* Low-dimensional problems (typically < 10 dimensions)
20+
* Smooth objective functions
21+
* Problems where derivatives are unavailable
22+
23+
Note: This is a local optimizer. For global optimization, consider
24+
using it with warm_start from a global optimizer's result.
25+
26+
Parameters
27+
----------
28+
param_space : dict[str, tuple]
29+
The search space to explore. Dictionary with parameter names as keys.
30+
Values must be tuples ``(low, high)`` for continuous ranges.
31+
32+
n_iter : int, default=100
33+
Maximum number of function evaluations.
34+
35+
max_time : float, optional
36+
Maximum optimization time in seconds.
37+
38+
initialize : dict, optional
39+
Initialization configuration. Supports:
40+
41+
* ``{"warm_start": [{"param1": val1, ...}, ...]}``: Start with
42+
known good configurations (uses first point as x0)
43+
44+
random_state : int, optional
45+
Random seed for initial point generation (if no warm_start).
46+
47+
xatol : float, default=1e-4
48+
Absolute error in parameter values for convergence.
49+
50+
fatol : float, default=1e-4
51+
Absolute error in objective function for convergence.
52+
53+
adaptive : bool, default=True
54+
Adapt algorithm parameters to dimensionality.
55+
56+
experiment : BaseExperiment, optional
57+
The experiment to optimize.
58+
59+
Attributes
60+
----------
61+
best_params_ : dict
62+
Best parameters found after calling ``solve()``.
63+
64+
best_score_ : float
65+
Score of the best parameters found.
66+
67+
See Also
68+
--------
69+
ScipyPowell : Another derivative-free local optimizer.
70+
ScipyBasinhopping : Global optimizer with local refinement.
71+
72+
References
73+
----------
74+
.. [1] Nelder, J. A., & Mead, R. (1965). A simplex method for function
75+
minimization. The computer journal, 7(4), 308-313.
76+
77+
Examples
78+
--------
79+
>>> from hyperactive.experiment.bench import Ackley
80+
>>> from hyperactive.opt.scipy import ScipyNelderMead
81+
82+
>>> ackley = Ackley.create_test_instance()
83+
>>> optimizer = ScipyNelderMead(
84+
... param_space={"x0": (-5.0, 5.0), "x1": (-5.0, 5.0)},
85+
... n_iter=200,
86+
... random_state=42,
87+
... experiment=ackley,
88+
... )
89+
>>> best_params = optimizer.solve() # doctest: +SKIP
90+
"""
91+
92+
_tags = {
93+
"info:name": "Scipy Nelder-Mead",
94+
"info:local_vs_global": "local",
95+
"info:explore_vs_exploit": "exploit",
96+
"info:compute": "low",
97+
"python_dependencies": ["scipy"],
98+
}
99+
100+
def __init__(
101+
self,
102+
param_space=None,
103+
n_iter=100,
104+
max_time=None,
105+
initialize=None,
106+
random_state=None,
107+
xatol=1e-4,
108+
fatol=1e-4,
109+
adaptive=True,
110+
experiment=None,
111+
):
112+
self.xatol = xatol
113+
self.fatol = fatol
114+
self.adaptive = adaptive
115+
116+
super().__init__(
117+
param_space=param_space,
118+
n_iter=n_iter,
119+
max_time=max_time,
120+
initialize=initialize,
121+
random_state=random_state,
122+
experiment=experiment,
123+
)
124+
125+
def _get_scipy_func(self):
126+
"""Get the minimize function.
127+
128+
Returns
129+
-------
130+
callable
131+
The ``scipy.optimize.minimize`` function.
132+
"""
133+
from scipy.optimize import minimize
134+
135+
return minimize
136+
137+
def _solve(self, experiment, param_space, n_iter, max_time=None, **kwargs):
138+
"""Run the Nelder-Mead optimization.
139+
140+
Overrides base class to use scipy.optimize.minimize with
141+
method='Nelder-Mead'.
142+
143+
Parameters
144+
----------
145+
experiment : BaseExperiment
146+
The experiment to optimize.
147+
param_space : dict
148+
The parameter space to search.
149+
n_iter : int
150+
Maximum number of function evaluations.
151+
max_time : float, optional
152+
Maximum time in seconds.
153+
**kwargs
154+
Additional parameters.
155+
156+
Returns
157+
-------
158+
dict
159+
Best parameters found.
160+
"""
161+
from scipy.optimize import minimize
162+
163+
# Convert search space
164+
bounds, param_names = self._convert_to_scipy_space(param_space)
165+
166+
# Create objective function (negated for minimization)
167+
def objective(x):
168+
params = self._array_to_dict(x, param_names)
169+
score = experiment(params)
170+
return -score
171+
172+
# Get initial point
173+
x0 = self._get_x0_from_initialize(bounds, param_names)
174+
if x0 is None:
175+
# Random initial point within bounds
176+
rng = np.random.RandomState(self.random_state)
177+
x0 = np.array([rng.uniform(low, high) for low, high in bounds])
178+
179+
# Set up options
180+
options = {
181+
"maxfev": n_iter,
182+
"xatol": self.xatol,
183+
"fatol": self.fatol,
184+
"adaptive": self.adaptive,
185+
}
186+
187+
# Run optimization
188+
result = minimize(
189+
objective,
190+
x0,
191+
method="Nelder-Mead",
192+
bounds=bounds,
193+
options=options,
194+
)
195+
196+
# Extract best parameters
197+
best_params = self._array_to_dict(result.x, param_names)
198+
self.best_score_ = -result.fun
199+
200+
return best_params
201+
202+
@classmethod
203+
def get_test_params(cls, parameter_set="default"):
204+
"""Return testing parameter settings for the optimizer.
205+
206+
Returns
207+
-------
208+
list of dict
209+
List of parameter configurations for testing.
210+
"""
211+
from hyperactive.experiment.bench import Ackley
212+
213+
params = []
214+
215+
ackley_exp = Ackley.create_test_instance()
216+
217+
# Test 1: Default configuration
218+
params.append(
219+
{
220+
"param_space": {
221+
"x0": (-5.0, 5.0),
222+
"x1": (-5.0, 5.0),
223+
},
224+
"n_iter": 100,
225+
"experiment": ackley_exp,
226+
"random_state": 42,
227+
}
228+
)
229+
230+
# Test 2: Tighter tolerances
231+
params.append(
232+
{
233+
"param_space": {
234+
"x0": (-5.0, 5.0),
235+
"x1": (-5.0, 5.0),
236+
},
237+
"n_iter": 200,
238+
"xatol": 1e-6,
239+
"fatol": 1e-6,
240+
"experiment": ackley_exp,
241+
"random_state": 42,
242+
}
243+
)
244+
245+
# Test 3: Non-adaptive
246+
params.append(
247+
{
248+
"param_space": {
249+
"x0": (-3.0, 3.0),
250+
"x1": (-3.0, 3.0),
251+
},
252+
"n_iter": 150,
253+
"adaptive": False,
254+
"experiment": ackley_exp,
255+
"random_state": 123,
256+
}
257+
)
258+
259+
return params

0 commit comments

Comments
 (0)