• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Bilkent-CYBORG / VOPy / 17138175823

21 Aug 2025 08:24PM UTC coverage: 96.012% (-0.8%) from 96.809%
17138175823

Pull #10

github

web-flow
Merge eddae9112 into 585aa3cef
Pull Request #10: PaVeBaGPOnline, Transforms, Optuna comparison

434 of 469 new or added lines in 18 files covered. (92.54%)

58 existing lines in 7 files now uncovered.

5152 of 5366 relevant lines covered (96.01%)

0.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.6
/vopy/maximization_problem.py
1
import warnings
1✔
2
from abc import ABC, abstractmethod
1✔
3
from typing import Callable, List, Optional, Tuple, Union
1✔
4

5
import numpy as np
1✔
6
from numpy.typing import ArrayLike
1✔
7

8
from vopy.datasets import Dataset
1✔
9
from vopy.utils import get_closest_indices_from_points, get_noisy_evaluations_chol
1✔
10

11

12
# TODO: Revise evaluate abstract method.
13
# It should both accomodate evaluations that are noisy in themselves and synthetically noisy ones.
14
# Maybe distinguish real problems from test problems.
15
class Problem(ABC):
1✔
16
    """
17
    Abstract base class for defining optimization problems. Provides a template
18
    for evaluating solutions in a given problem space.
19

20
    .. note::
21
        Classes derived from :class:`Problem` must implement the :meth:`evaluate` method. Also,
22
        they should have the following attributes defined:
23

24
    - :obj:`in_dim`: :type:`int`
25
    - :obj:`out_dim`: :type:`int`
26
    - :obj:`bounds`: :type:`List[Tuple[float, float]]`
27
    """
28

29
    in_dim: int
1✔
30
    out_dim: int
1✔
31
    bounds: Union[np.ndarray, List[Tuple[float, float]]]
1✔
32

33
    def __init__(self) -> None:
1✔
34
        super().__init__()
1✔
35

36
    @abstractmethod
37
    def evaluate(self, x: np.ndarray) -> np.ndarray:
38
        """
39
        Evaluates the problem at a given point (or array of points) :obj:`x`.
40

41
        :param x: The input for where to evaluate the problem.
42
        :type x: np.ndarray
43
        :return: The evaluation result as an array, representing the objective values at :obj:`x`.
44
        :rtype: np.ndarray
45
        """
46
        pass
47

48

49
class FixedPointsProblem(Problem):
1✔
50
    """
51
    Define an evaluatable optimization problem using fixed points and an objective function. Noise
52
    is assumed to be intrinsic to the objective function.
53

54
    :param in_points: The fixed input points to evaluate the objective function.
55
    :type in_points: ArrayLike
56
    :param out_dim: The dimension of the output space of the objective function.
57
    :type out_dim: int
58
    :param objective: The objective function to evaluate at the fixed input points.
59
        Defaults to `None`.
60
    :type objective: Optional[Callable[[ArrayLike], ArrayLike]]
61
    :param bounds: Optional bounds for the input points, given as a 2D array of shape (in_dim, 2).
62
        If not provided, bounds are inferred from the input points.
63
    :type bounds: Optional[np.ndarray]
64
    """
65

66
    def __init__(
1✔
67
        self,
68
        in_points: ArrayLike,
69
        out_dim: int,
70
        objective: Optional[Callable[[ArrayLike], ArrayLike]] = None,
71
        bounds: Optional[np.ndarray] = None,
72
    ) -> None:
73
        self.in_data = np.array(in_points)
1✔
74
        self.objective = objective
1✔
75

76
        self.in_dim = self.in_data.shape[1]
1✔
77
        self.out_dim = out_dim
1✔
78

79
        mins = np.min(self.in_data, axis=0, keepdims=True)
1✔
80
        maxs = np.max(self.in_data, axis=0, keepdims=True)
1✔
81
        if bounds is not None:
1✔
82
            if bounds.shape != (self.in_dim, 2):
1✔
NEW
83
                raise ValueError("Bounds must be a 2D array of shape (in_dim, 2).")
×
84
            self.bounds = bounds
1✔
85
        else:
86
            self.bounds = np.concatenate([mins.reshape(-1, 1), maxs.reshape(-1, 1)], axis=1)
1✔
87

88
        super().__init__()
1✔
89

90
    def evaluate(self, x: np.ndarray) -> np.ndarray:
1✔
91
        """
92
        Evaluates the problem at given points.
93

94
        :param x: The input points to evaluate, given as an array of shape (N, in_dim).
95
        :type x: np.ndarray
96
        :return: An array of shape (N, out_dim) representing the evaluated output.
97
        :rtype: np.ndarray
98
        """
99
        if self.objective is None:
1✔
NEW
100
            raise ValueError("Objective function for this FixedPointProblem is not defined.")
×
101

102
        if x.ndim <= 1:
1✔
NEW
103
            x = x.reshape(1, -1)
×
104

105
        if x.ndim != 2:
1✔
NEW
106
            raise ValueError("Input points must be 2D array.")
×
107

108
        n_evals = x.shape[0]
1✔
109

110
        y = np.empty((n_evals, self.out_dim))
1✔
111
        for eval_i in range(n_evals):
1✔
112
            y[eval_i] = self.objective(x[eval_i])
1✔
113

114
        return y
1✔
115

116

117
class ProblemFromDataset(FixedPointsProblem):
1✔
118
    """
119
    Define an evaluatable optimization problem using data from a given dataset.
120

121
    This class enables the evaluation of points based on nearest neighbor lookup
122
    from an offline dataset, with optional Gaussian noise.
123

124
    :param dataset: The dataset containing input and output data for the problem.
125
    :type dataset: Dataset
126
    :param noise_var: The variance of the noise to add to the outputs.
127
    :type noise_var: float
128
    """
129

130
    def __init__(self, dataset: Dataset, noise_var: float) -> None:
1✔
131
        self.in_dim = dataset._in_dim
1✔
132
        self.out_dim = dataset._out_dim
1✔
133
        self.bounds = np.array([(0, 1)] * dataset._in_dim)
1✔
134

135
        self.dataset = dataset
1✔
136
        self.noise_var = noise_var
1✔
137

138
        super().__init__(in_points=self.dataset.in_data, out_dim=self.out_dim, bounds=self.bounds)
1✔
139

140
        noise_covar = np.eye(self.out_dim) * noise_var
1✔
141
        self.noise_cholesky = np.linalg.cholesky(noise_covar)
1✔
142

143
    def evaluate(self, x: np.ndarray, noisy: bool = True) -> np.ndarray:
1✔
144
        """
145
        Evaluates the problem at given points by finding the nearest points
146
        in the dataset and optionally adding Gaussian noise.
147

148
        :param x: The input points to evaluate, given as an array of shape (N, in_dim).
149
        :type x: np.ndarray
150
        :param noisy: If `True`, adds Gaussian noise to the output based on the specified
151
            noise variance. Defaults to `True`.
152
        :type noisy: bool
153
        :return: An array of shape (N, out_dim) representing the evaluated output.
154
        :rtype: np.ndarray
155
        """
156
        if x.ndim <= 1:
1✔
157
            x = x.reshape(1, -1)
1✔
158

159
        indices = get_closest_indices_from_points(x, self.dataset.in_data, squared=True)
1✔
160
        f = self.dataset.out_data[indices].reshape(len(x), -1)
1✔
161

162
        if not noisy:
1✔
163
            return f
1✔
164

165
        y = get_noisy_evaluations_chol(f, self.noise_cholesky)
1✔
166
        return y
1✔
167

168

169
class ContinuousProblem(Problem):
1✔
170
    """
171
    Abstract base class for continuous optimization problems. It includes noise handling for
172
    outputs based on a specified noise variance.
173

174
    :param noise_var: The variance of the noise to be added to the outputs. If the problem has
175
        intrinsic noise, this should be set to `None`. Defaults to `None`.
176
    :type noise_var: Optional[float]
177
    """
178

179
    def __init__(self, noise_var: Optional[float]) -> None:
1✔
180
        super().__init__()
1✔
181

182
        self.noise_var = noise_var
1✔
183

184
        if noise_var is not None:
1✔
185
            noise_covar = np.eye(self.out_dim) * noise_var
1✔
186
            self.noise_cholesky = np.linalg.cholesky(noise_covar)
1✔
187
        else:
NEW
188
            self.noise_cholesky = None
×
189

190
    @abstractmethod
191
    def evaluate_true(self, x: np.ndarray) -> np.ndarray:
192
        pass
193

194
    def evaluate(self, x: np.ndarray, noisy: bool = True) -> np.ndarray:
1✔
195
        """
196
        Evaluates the problem at given points with optional Gaussian noise.
197

198
        :param x: Input points to evaluate, given as an array of shape (N, 2).
199
        :type x: np.ndarray
200
        :param noisy: If `True`, adds Gaussian noise to the output based on the specified
201
            noise variance. Defaults to `True`.
202
        :type noisy: bool
203
        :return: A 2D array with evaluated Branin and Currin values for each input,
204
            with optional noise.
205
        :rtype: np.ndarray
206
        """
207
        if x.ndim == 1:
1✔
208
            x = x.reshape(1, -1)
1✔
209

210
        f = self.evaluate_true(x)
1✔
211

212
        if not noisy:
1✔
213
            return f
1✔
214

215
        if self.noise_cholesky is None:
1✔
NEW
216
            warnings.warn(
×
217
                "No noise applied to the output because noise_cholesky is None."
218
                " This may indicate that the problem has intrinsic noise."
219
                " If that is the case, call with `noisy=False`."
220
            )
NEW
221
            y = f
×
222
        else:
223
            y = get_noisy_evaluations_chol(f, self.noise_cholesky)
1✔
224
        return y
1✔
225

226

227
def get_continuous_problem(name: str, noise_var: Optional[float] = None) -> ContinuousProblem:
1✔
228
    """
229
    Retrieves an instance of a continuous problem by name. If the
230
    problem name is not recognized, a ValueError is raised.
231

232
    :param name: The name of the continuous problem class to instantiate.
233
    :type name: str
234
    :param noise_var: The variance of the noise to apply in the problem. If `None`, no additive
235
        noise is applied. Defaults to `None`.
236
    :type noise_var: Optional[float]
237
    :return: An instance of the specified continuous problem.
238
    :rtype: ContinuousProblem
239

240
    :raises ValueError: If the specified problem name does not exist in the global scope.
241
    """
242
    if name in globals():
1✔
243
        return globals()[name](noise_var)
1✔
244

245
    raise ValueError(f"Unknown continuous problem: {name}")
1✔
246

247

248
class BraninCurrin(ContinuousProblem):
1✔
249
    """
250
    A continuous optimization problem combining the Branin and Currin functions.
251
    This problem was first utilized by [Belakaria2019]_ for multi-objective
252
    optimization tasks, where both objectives are evaluated over the same input domain.
253

254
    :param noise_var: The variance of the noise added to the output evaluations.
255
    :type noise_var: float
256

257
    References:
258
        .. [Belakaria2019]
259
            Belakaria, Deshwal, Doppa.
260
            Max-value Entropy Search for Multi-Objective Bayesian Optimization.
261
            Neural Information Processing Systems (NeurIPS), 2019.
262
    """
263

264
    bounds = np.array([(0.0, 1.0), (0.0, 1.0)])
1✔
265
    in_dim = len(bounds)
1✔
266
    out_dim = 2
1✔
267
    domain_discretization_each_dim = 33
1✔
268
    depth_max = 5
1✔
269

270
    def __init__(self, noise_var: float) -> None:
1✔
271
        super().__init__(noise_var)
1✔
272

273
    def _branin(self, X):
1✔
274
        """
275
        Computes the Branin function.
276

277
        :param X: The input array of shape (N, 2).
278
        :type X: np.ndarray
279
        :return: The evaluated Branin function values.
280
        :rtype: np.ndarray
281
        """
282
        x_0 = 15 * X[..., 0] - 5
1✔
283
        x_1 = 15 * X[..., 1]
1✔
284
        X = np.stack([x_0, x_1], axis=1)
1✔
285

286
        t1 = X[..., 1] - 5.1 / (4 * np.pi**2) * X[..., 0] ** 2 + 5 / np.pi * X[..., 0] - 6
1✔
287
        t2 = 10 * (1 - 1 / (8 * np.pi)) * np.cos(X[..., 0])
1✔
288
        return t1**2 + t2 + 10
1✔
289

290
    def _currin(self, X):
1✔
291
        """
292
        Computes the Currin function.
293

294
        :param X: The input array of shape (N, 2).
295
        :type X: np.ndarray
296
        :return: The evaluated Currin function values.
297
        :rtype: np.ndarray
298
        """
299
        x_0 = X[..., 0]
1✔
300
        x_1 = X[..., 1]
1✔
301
        x_1[x_1 == 0] += 1e-9
1✔
302
        factor1 = 1 - np.exp(-1 / (2 * x_1))
1✔
303
        numer = 2300 * np.power(x_0, 3) + 1900 * np.power(x_0, 2) + 2092 * x_0 + 60
1✔
304
        denom = 100 * np.power(x_0, 3) + 500 * np.power(x_0, 2) + 4 * x_0 + 20
1✔
305
        return factor1 * numer / denom
1✔
306

307
    def evaluate_true(self, x: np.ndarray) -> np.ndarray:
1✔
308
        """
309
        Evaluates the true (noiseless) outputs of the Branin and Currin functions,
310
        normalized for each output dimension.
311

312
        :param x: Input points to evaluate, with shape (N, 2).
313
        :type x: np.ndarray
314
        :return: A 2D array with normalized Branin and Currin function values for each input.
315
        :rtype: np.ndarray
316
        """
317
        branin = self._branin(x)
1✔
318
        currin = self._currin(x)
1✔
319

320
        # Normalize the results
321
        branin = (branin - 54.3669) / 51.3086
1✔
322
        currin = (currin - 7.5926) / 2.6496
1✔
323

324
        Y = np.stack([-branin, -currin], axis=1)
1✔
325
        return Y
1✔
326

327

328
class DecoupledEvaluationProblem(Problem):
1✔
329
    """
330
    Wrapper around a :class:`Problem` instance that allows for decoupled evaluations of
331
    objective functions. This class enables selective evaluation of specific objectives
332
    by indexing into the output of the underlying problem. Therefore, this class is not
333
    suitable for real-time evaluations of the underlying problem.
334

335
    :param problem: An instance of :class:`Problem` to wrap and decouple evaluations.
336
    :type problem: Problem
337
    """
338

339
    def __init__(self, problem: Problem) -> None:
1✔
340
        super().__init__()
1✔
341
        self.problem = problem
1✔
342

343
    def evaluate(
1✔
344
        self,
345
        x: np.ndarray,
346
        evaluation_index: Optional[Union[int, List[int]]] = None,
347
        **evaluate_kwargs: dict,
348
    ) -> np.ndarray:
349
        """
350
        Evaluates the underlying problem at the given points and returns either the full
351
        output or specific objectives as specified by `evaluation_index`.
352

353
        :param x: The input points to evaluate, given as an array of shape (N, in_dim).
354
        :type x: np.ndarray
355
        :param evaluation_index: Specifies which objectives to return. Can be:
356
            - `None` (default) to return all objectives,
357
            - an `int` to return a specific objective across all points,
358
            - a list of indices to return specific objectives for each point.
359
        :type evaluation_index: Optional[Union[int, List[int]]]
360
        :param evaluate_kwargs: Additional keyword arguments to pass to the evaluation function of
361
            the underlying problem.
362
        :type evaluate_kwargs: dict
363
        :return: An array of evaluated values, either the full output or specific objectives.
364
        :rtype: np.ndarray
365
        :raises ValueError: If :obj:`evaluation_index` has an invalid format or length.
366
        """
367
        if (
1✔
368
            evaluation_index is not None
369
            and not isinstance(evaluation_index, int)
370
            and len(x) != len(evaluation_index)
371
        ):
372
            raise ValueError(
×
373
                "evaluation_index must; be None, have type int or have the same length as x."
374
            )
375

376
        values = self.problem.evaluate(x, **evaluate_kwargs)
1✔
377

378
        if evaluation_index is None:
1✔
379
            return values
1✔
380

381
        if isinstance(evaluation_index, int):
1✔
382
            return values[:, evaluation_index]
1✔
383

384
        evaluation_index = np.array(evaluation_index, dtype=np.int32)
1✔
385
        return values[np.arange(len(evaluation_index)), evaluation_index]
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc