Skip to content

Commit 83459ce

Browse files
Refactor acquisition functions to use functools.partial instead of lambda's. Goal: enable pickling
Moreover: - Create greedy approach for FarEnoughSampleFilter. More reliable as a fallback - PartialVariableFunction as a common interface for MinimizeSurrogate and cptv - Record all points used in the local solver to improve the surrogate in cptv-L
1 parent ddec626 commit 83459ce

22 files changed

+352
-166
lines changed

examples/vlse_benchmark/vlse_bench.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,15 +127,15 @@ def run_optimizer(
127127
maxeval=maxEval - 2 * (nArgs + 1),
128128
surrogateModel=modelIter,
129129
acquisitionFunc=acquisitionFuncIter,
130-
seed=2*i+1,
130+
seed=2 * i + 1,
131131
)
132132
else:
133133
res = optimizer(
134134
objf,
135135
bounds=bounds,
136136
maxeval=maxEval - 2 * (nArgs + 1),
137137
surrogateModel=modelIter,
138-
seed=2*i+1,
138+
seed=2 * i + 1,
139139
)
140140
optres.append(res)
141141

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ test = [
4545
"jupyter",
4646
"ipykernel",
4747
"pygoblet",
48-
"PyNomadBBO",
4948
]
5049
lint = [
5150
"ruff",

soogo/acquisition/endpoints_pareto_front.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import numpy as np
2424
from typing import Optional
25+
import functools
2526

2627
from pymoo.optimize import minimize as pymoo_minimize
2728

@@ -88,7 +89,7 @@ def optimize(
8889
endpoints = np.empty((0, dim))
8990
for i in range(objdim):
9091
minimumPointProblem = PymooProblem(
91-
lambda x: surrogateModel(x, i=i), bounds, iindex
92+
functools.partial(surrogateModel, i=i), bounds, iindex
9293
)
9394
res = pymoo_minimize(
9495
minimumPointProblem,

soogo/acquisition/maximize_distance.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import numpy as np
2121
from typing import Optional
22+
import functools
2223

2324
from pymoo.optimize import minimize as pymoo_minimize
2425

@@ -28,6 +29,16 @@
2829
from .utils import FarEnoughSampleFilter
2930

3031

32+
def _negative_distance(tree, x):
33+
"""Compute negative distance to nearest neighbor.
34+
35+
:param tree: KDTree of previously sampled points.
36+
:param x: Point to evaluate.
37+
:return: Negative distance to nearest neighbor.
38+
"""
39+
return -tree.query(x)[0]
40+
41+
3142
class MaximizeDistance(Acquisition):
3243
"""
3344
Maximizing distance acquisition function as described in [#]_.
@@ -86,7 +97,7 @@ def optimize(
8697
filter = FarEnoughSampleFilter(exclusion_set, self.tol(bounds))
8798

8899
problem = PymooProblem(
89-
lambda x: -filter.tree.query(x)[0],
100+
functools.partial(_negative_distance, filter.tree),
90101
bounds,
91102
iindex,
92103
)

soogo/acquisition/maximize_ei.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from scipy.linalg import cholesky, solve_triangular
2323
from typing import Optional
2424
import logging
25+
import functools
2526

2627
from pymoo.optimize import minimize as pymoo_minimize
2728

@@ -34,6 +35,17 @@
3435
logger = logging.getLogger(__name__)
3536

3637

38+
def _negative_ei(surrogate, best_value, x):
39+
"""Negative expected improvement (for minimization).
40+
41+
:param surrogate: Surrogate model.
42+
:param best_value: Best objective value so far.
43+
:param x: Point to evaluate.
44+
:return: Negative expected improvement at x.
45+
"""
46+
return -surrogate.expected_improvement(x, best_value)
47+
48+
3749
class MaximizeEI(Acquisition):
3850
"""Acquisition by maximizing the expected improvement (EI) of a
3951
:class:`.GaussianProcess`.
@@ -156,7 +168,7 @@ def optimize(
156168

157169
# Use the point that maximizes the EI
158170
problem = PymooProblem(
159-
lambda x: -surrogateModel.expected_improvement(x, ybest),
171+
functools.partial(_negative_ei, surrogateModel, ybest),
160172
bounds,
161173
)
162174
res = pymoo_minimize(

soogo/acquisition/minimize_surrogate.py

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,40 +25,24 @@
2525
from scipy.optimize import minimize
2626
from scipy.stats.qmc import LatinHypercube
2727
from typing import Optional
28+
import functools
2829

2930
from .base import Acquisition
3031
from ..model import Surrogate
3132
from ..sampling import random_sample
3233
from .utils import FarEnoughSampleFilter
34+
from ..optimize.utils import PartialVariableFunction
3335

3436

35-
class _SurrogateMinimizerWrapper:
36-
"""Helper class to wrap surrogate model for minimization.
37+
def _ith_function_value(fun, x, i):
38+
"""Returns the i-th component of the function value at x.
3739
38-
:param surrogateModel: Surrogate model to be minimized.
39-
:param x: Base point in the space.
40-
:param i: Index(s) that will be optimized.
40+
:param fun: Function that computes the function value at x.
41+
:param x: Point at which to evaluate the function.
42+
:param i: Index of the component to return.
43+
:return: i-th component of the function value at x.
4144
"""
42-
43-
def __init__(self, surrogateModel, x, i):
44-
self.surrogateModel = surrogateModel
45-
self.x = x
46-
self.i = i
47-
48-
def fun(self, xi):
49-
"""Evaluate surrogate model at modified point x[i] = xi."""
50-
_x = self.x.copy()
51-
_x[self.i] = xi
52-
return self.surrogateModel(_x)
53-
54-
def jac(self, xi):
55-
"""Evaluate surrogate model Jacobian at modified point x[i] = xi.
56-
57-
Only the components corresponding to indices i are returned.
58-
"""
59-
_x = self.x.copy()
60-
_x[self.i] = xi
61-
return self.surrogateModel.jac(_x)[self.i]
45+
return fun(x)[i]
6246

6347

6448
class MinimizeSurrogate(Acquisition):
@@ -144,8 +128,15 @@ def optimize(
144128
critdist = (
145129
(gamma(1 + (dim / 2)) * volumeBounds * sigma) ** (1 / dim)
146130
) / np.sqrt(np.pi) # critical distance when 2 points are equal
147-
has_jac = hasattr(surrogateModel, "jac") and callable(
148-
getattr(surrogateModel, "jac")
131+
model_jac = (
132+
functools.partial(
133+
_ith_function_value, surrogateModel.jac, i=cindex
134+
)
135+
if (
136+
hasattr(surrogateModel, "jac")
137+
and callable(getattr(surrogateModel, "jac"))
138+
)
139+
else None
149140
)
150141

151142
# Local space to store information
@@ -220,14 +211,19 @@ def optimize(
220211
for i in range(nSelected):
221212
xi = candidates[chosenIds[i], :]
222213

223-
wrapper = _SurrogateMinimizerWrapper(
214+
wrapper_fun = PartialVariableFunction(
224215
surrogateModel, xi, cindex
225216
)
217+
wrapper_jac = (
218+
PartialVariableFunction(model_jac, xi, cindex)
219+
if model_jac is not None
220+
else None
221+
)
226222
res = minimize(
227-
wrapper.fun,
223+
wrapper_fun,
228224
xi[cindex],
229225
method="L-BFGS-B",
230-
jac=wrapper.jac if has_jac else None,
226+
jac=wrapper_jac,
231227
bounds=cbounds,
232228
options={
233229
"maxfun": remevals,

soogo/acquisition/pareto_front.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import numpy as np
2222
from scipy.spatial import KDTree
2323
from typing import Optional
24+
import functools
2425

2526
from pymoo.core.initialization import Initialization
2627
from pymoo.optimize import minimize as pymoo_minimize
@@ -33,6 +34,17 @@
3334
from .utils import FarEnoughSampleFilter
3435

3536

37+
def _distance_to_target(surrogate, target_value, x):
38+
"""Compute distance from surrogate prediction to target.
39+
40+
:param surrogate: Surrogate model.
41+
:param target_value: Target value to compare against.
42+
:param x: Point to evaluate.
43+
:return: Distance from surrogate prediction at x to target_value.
44+
"""
45+
return np.absolute(surrogate(x) - target_value)
46+
47+
3648
class ParetoFront(Acquisition):
3749
"""Obtain sample points that fill gaps in the Pareto front from [#]_.
3850
@@ -231,7 +243,7 @@ def optimize(
231243
# For discontinuous Pareto fronts in the original problem, such set
232244
# may not exist, or it may be too far from the target value.
233245
multiobjTVProblem = PymooProblem(
234-
lambda x: np.absolute(surrogateModel(x) - tau),
246+
functools.partial(_distance_to_target, surrogateModel, tau),
235247
bounds,
236248
iindex,
237249
n_obj=objdim,

soogo/acquisition/target_value_acquisition.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import numpy as np
2121
import logging
2222
from typing import Optional
23+
import functools
2324

2425
from pymoo.optimize import minimize as pymoo_minimize
2526

@@ -268,8 +269,11 @@ def optimize(
268269
surrogateModel.prepare_mu_measure()
269270
mu_measure_is_prepared = True
270271
problem = PymooProblem(
271-
lambda x: TargetValueAcquisition.bumpiness_measure(
272-
surrogateModel, x, f_target, target_range
272+
functools.partial(
273+
TargetValueAcquisition.bumpiness_measure,
274+
surrogateModel,
275+
target=f_target,
276+
target_range=target_range,
273277
),
274278
bounds,
275279
iindex,
@@ -305,8 +309,11 @@ def optimize(
305309
surrogateModel.prepare_mu_measure()
306310
mu_measure_is_prepared = True
307311
problem = PymooProblem(
308-
lambda x: TargetValueAcquisition.bumpiness_measure(
309-
surrogateModel, x, f_target, target_range
312+
functools.partial(
313+
TargetValueAcquisition.bumpiness_measure,
314+
surrogateModel,
315+
target=f_target,
316+
target_range=target_range,
310317
),
311318
bounds,
312319
iindex,

soogo/acquisition/utils.py

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,10 @@ class FarEnoughSampleFilter:
230230
231231
:param X: Matrix of existing sample points (n x d).
232232
:param tol: Minimum distance threshold.
233+
:param approach: Method to select points when multiple candidates are close
234+
to each other. Options are:
235+
"independent_set" (default) which uses a maximum independent set
236+
algorithm, and "greedy" which uses a simple greedy approach.
233237
234238
.. attribute:: tree
235239
@@ -239,11 +243,17 @@ class FarEnoughSampleFilter:
239243
.. attribute:: tol
240244
241245
Minimum distance threshold. Points closer than this are filtered out.
246+
247+
.. attribute:: approach
248+
249+
Method to select points when multiple candidates are close to each
250+
other.
242251
"""
243252

244-
def __init__(self, X, tol):
253+
def __init__(self, X, tol, approach="independent_set"):
245254
self.tree = KDTree(X)
246255
self.tol = tol
256+
self.approach = approach
247257

248258
def is_far_enough(self, x):
249259
"""Check if a point is far enough from existing samples.
@@ -254,6 +264,28 @@ def is_far_enough(self, x):
254264
dist, _ = self.tree.query(x.reshape(1, -1))
255265
return dist[0] >= self.tol
256266

267+
def max_independent_set(self, X):
268+
dist = cdist(X, X)
269+
np.fill_diagonal(dist, np.inf)
270+
g = nx.Graph()
271+
g.add_nodes_from(range(len(X)))
272+
g.add_edges_from(
273+
[
274+
(i, j)
275+
for i in range(len(X))
276+
for j in range(i + 1, len(X))
277+
if dist[i, j] < self.tol
278+
]
279+
)
280+
return maximum_independent_set(g)
281+
282+
def greedy_independent_set(self, X):
283+
idx = []
284+
for i in range(len(X)):
285+
if all(np.linalg.norm(X[i] - X[j]) >= self.tol for j in idx):
286+
idx.append(i)
287+
return idx
288+
257289
def indices(self, Xc):
258290
"""Filter candidates based on minimum distance criterion.
259291
@@ -266,19 +298,18 @@ def indices(self, Xc):
266298
Xc0 = Xc[mask0]
267299

268300
# Find the maximum independent set among the remaining points
269-
dist = cdist(Xc0, Xc0)
270-
np.fill_diagonal(dist, np.inf)
271-
g = nx.Graph()
272-
g.add_nodes_from(range(len(Xc0)))
273-
g.add_edges_from(
274-
[
275-
(i, j)
276-
for i in range(len(Xc0))
277-
for j in range(i + 1, len(Xc0))
278-
if dist[i, j] < self.tol
279-
]
280-
)
281-
idx = maximum_independent_set(g)
301+
if self.approach == "independent_set":
302+
try:
303+
idx = self.max_independent_set(Xc0)
304+
except Exception as e:
305+
logger.warning(
306+
"Error in maximum independent set computation: %s. "
307+
"Falling back to a greedy approach.",
308+
e,
309+
)
310+
idx = self.greedy_independent_set(Xc0)
311+
else:
312+
idx = self.greedy_independent_set(Xc0)
282313

283314
# Recover original indices
284315
original_indices = np.where(mask0)[0]

0 commit comments

Comments
 (0)