diff --git a/docs/source/index.rst b/docs/source/index.rst index e2bb64b..24e5c6c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -28,14 +28,15 @@ Welcome to VectOptAL, an open-source Python library built to tackle the challeng :maxdepth: 1 :caption: Package Reference - order - models algorithms + models + order + ordering_cone acquisition - datasets - utils design_space confidence_region + datasets + utils Indices and tables diff --git a/vectoptal/datasets/dataset.py b/vectoptal/datasets/dataset.py index b298138..b07b484 100644 --- a/vectoptal/datasets/dataset.py +++ b/vectoptal/datasets/dataset.py @@ -31,9 +31,9 @@ class Dataset(ABC): Abstract base class for datasets that handles min-max scaling of input and standardization of output. Any class inheriting from this class should implement the following properties: - - _in_dim: int - - _out_dim: int - - _cardinality: int + - :obj:`_in_dim`: :type:`int` + - :obj:`_out_dim`: :type:`int` + - :obj:`_cardinality`: :type:`int` """ @property diff --git a/vectoptal/design_space.py b/vectoptal/design_space.py index 18a1fd9..f4d264e 100644 --- a/vectoptal/design_space.py +++ b/vectoptal/design_space.py @@ -5,6 +5,7 @@ import numpy as np from vectoptal.confidence_region import ( + ConfidenceRegion, RectangularConfidenceRegion, EllipsoidalConfidenceRegion, ) @@ -42,10 +43,22 @@ class DiscreteDesignSpace(DesignSpace): This class is an abstract implementation of the `DesignSpace` abstract base class. It represents a design space where the points are discrete. The class also maintains a list of confidence regions associated with the design points. + + A derived class must define the following properties: + + - :obj:`points`: :type:`np.ndarray` + - :obj:`confidence_regions`: :type:`list[ConfidenceRegion]` """ - points: np.ndarray - confidence_regions: list + @property + @abstractmethod + def points(self) -> np.ndarray: + pass + + @property + @abstractmethod + def confidence_regions(self) -> list[ConfidenceRegion]: + pass def __init__(self): super().__init__() diff --git a/vectoptal/maximization_problem.py b/vectoptal/maximization_problem.py index d611df6..1e3850c 100644 --- a/vectoptal/maximization_problem.py +++ b/vectoptal/maximization_problem.py @@ -11,15 +11,44 @@ # It should both accomodate evaluations that are noisy in themselves and synthetically noisy ones. # Maybe distinguish real problems from test problems. class Problem(ABC): + """ + Abstract base class for defining optimization problems. Provides a template + for evaluating solutions in a given problem space. + + .. note:: + Classes derived from :class:`Problem` must implement the :meth:`evaluate` method. + + """ + def __init__(self) -> None: super().__init__() @abstractmethod def evaluate(self, x: np.ndarray) -> np.ndarray: + """ + Evaluates the problem at a given point (or array of points) :obj:`x`. + + :param x: The input for where to evaluate the problem. + :type x: np.ndarray + :return: The evaluation result as an array, representing the objective values at :obj:`x`. + :rtype: np.ndarray + """ pass class ProblemFromDataset(Problem): + """ + Define an evaluatable optimization problem using data from a given dataset. + + This class enables the evaluation of points based on nearest neighbor lookup + from an offline dataset, with optional Gaussian noise. + + :param dataset: The dataset containing input and output data for the problem. + :type dataset: Dataset + :param noise_var: The variance of the noise to add to the outputs. + :type noise_var: float + """ + def __init__(self, dataset: Dataset, noise_var: float) -> None: super().__init__() @@ -30,6 +59,18 @@ def __init__(self, dataset: Dataset, noise_var: float) -> None: self.noise_cholesky = np.linalg.cholesky(noise_covar) def evaluate(self, x: np.ndarray, noisy: bool = True) -> np.ndarray: + """ + Evaluates the problem at given points by finding the nearest points + in the dataset and optionally adding Gaussian noise. + + :param x: The input points to evaluate, given as an array of shape (N, in_dim). + :type x: np.ndarray + :param noisy: If `True`, adds Gaussian noise to the output based on the specified + noise variance. Defaults to `True`. + :type noisy: bool + :return: An array of shape (N, out_dim) representing the evaluated output. + :rtype: np.ndarray + """ if x.ndim <= 1: x = x.reshape(1, -1) @@ -44,6 +85,21 @@ def evaluate(self, x: np.ndarray, noisy: bool = True) -> np.ndarray: class ContinuousProblem(Problem): + """ + Abstract base class for continuous optimization problems. It includes noise handling for + outputs based on a specified noise variance. It should have the following property defined: + + - :obj:`out_dim`: :type:`int` + + :param noise_var: The variance of the noise to be added to the outputs. + :type noise_var: float + """ + + @property + @abstractmethod + def out_dim(self) -> int: + pass + def __init__(self, noise_var: float) -> None: super().__init__() @@ -54,6 +110,19 @@ def __init__(self, noise_var: float) -> None: def get_continuous_problem(name: str, noise_var: float) -> ContinuousProblem: + """ + Retrieves an instance of a continuous problem by name. If the + problem name is not recognized, a ValueError is raised. + + :param name: The name of the continuous problem class to instantiate. + :type name: str + :param noise_var: The variance of the noise to apply in the problem. + :type noise_var: float + :return: An instance of the specified continuous problem. + :rtype: ContinuousProblem + + :raises ValueError: If the specified problem name does not exist in the global scope. + """ if name in globals(): return globals()[name](noise_var) @@ -61,6 +130,21 @@ def get_continuous_problem(name: str, noise_var: float) -> ContinuousProblem: class BraninCurrin(ContinuousProblem): + """ + A continuous optimization problem combining the Branin and Currin functions. + This problem was first utilized by [Belakaria2019]_ for multi-objective + optimization tasks, where both objectives are evaluated over the same input domain. + + :param noise_var: The variance of the noise added to the output evaluations. + :type noise_var: float + + References: + .. [Belakaria2019] + Belakaria, Deshwal, Doppa. + Max-value Entropy Search for Multi-Objective Bayesian Optimization. + Neural Information Processing Systems (NeurIPS), 2019. + """ + bounds = [(0.0, 1.0), (0.0, 1.0)] in_dim = len(bounds) out_dim = 2 @@ -71,6 +155,14 @@ def __init__(self, noise_var: float) -> None: super().__init__(noise_var) def _branin(self, X): + """ + Computes the Branin function. + + :param X: The input array of shape (N, 2). + :type X: np.ndarray + :return: The evaluated Branin function values. + :rtype: np.ndarray + """ x_0 = 15 * X[..., 0] - 5 x_1 = 15 * X[..., 1] X = np.stack([x_0, x_1], axis=1) @@ -80,6 +172,14 @@ def _branin(self, X): return t1**2 + t2 + 10 def _currin(self, X): + """ + Computes the Currin function. + + :param X: The input array of shape (N, 2). + :type X: np.ndarray + :return: The evaluated Currin function values. + :rtype: np.ndarray + """ x_0 = X[..., 0] x_1 = X[..., 1] x_1[x_1 == 0] += 1e-9 @@ -89,6 +189,15 @@ def _currin(self, X): return factor1 * numer / denom def evaluate_true(self, x: np.ndarray) -> np.ndarray: + """ + Evaluates the true (noiseless) outputs of the Branin and Currin functions, + normalized for each output dimension. + + :param x: Input points to evaluate, with shape (N, 2). + :type x: np.ndarray + :return: A 2D array with normalized Branin and Currin function values for each input. + :rtype: np.ndarray + """ branin = self._branin(x) currin = self._currin(x) @@ -100,6 +209,18 @@ def evaluate_true(self, x: np.ndarray) -> np.ndarray: return Y def evaluate(self, x: np.ndarray, noisy: bool = True) -> np.ndarray: + """ + Evaluates the problem at given points with optional Gaussian noise. + + :param x: Input points to evaluate, given as an array of shape (N, 2). + :type x: np.ndarray + :param noisy: If `True`, adds Gaussian noise to the output based on the specified + noise variance. Defaults to `True`. + :type noisy: bool + :return: A 2D array with evaluated Branin and Currin values for each input, + with optional noise. + :rtype: np.ndarray + """ if x.ndim == 1: x = x.reshape(1, -1) @@ -113,6 +234,15 @@ def evaluate(self, x: np.ndarray, noisy: bool = True) -> np.ndarray: class DecoupledEvaluationProblem(Problem): + """ + Wrapper around a :class:`Problem` instance that allows for decoupled evaluations of + objective functions. This class enables selective evaluation of specific objectives + by indexing into the output of the underlying problem. + + :param problem: An instance of :class:`Problem` to wrap and decouple evaluations. + :type problem: Problem + """ + def __init__(self, problem: Problem) -> None: super().__init__() self.problem = problem @@ -120,6 +250,30 @@ def __init__(self, problem: Problem) -> None: def evaluate( self, x: np.ndarray, evaluation_index: Optional[Union[int, List[int]]] = None ) -> np.ndarray: + """ + Evaluates the underlying problem at the given points and returns either the full + output or specific objectives as specified by `evaluation_index`. + + :param x: The input points to evaluate, given as an array of shape (N, in_dim). + :type x: np.ndarray + :param evaluation_index: Specifies which objectives to return. Can be: + - `None` (default) to return all objectives, + - an `int` to return a specific objective across all points, + - a list of indices to return specific objectives for each point. + :type evaluation_index: Optional[Union[int, List[int]]] + :return: An array of evaluated values, either the full output or specific objectives. + :rtype: np.ndarray + :raises ValueError: If :obj:`evaluation_index` has an invalid format or length. + """ + if ( + evaluation_index is not None + and not isinstance(evaluation_index, int) + and len(x) != len(evaluation_index) + ): + raise ValueError( + "evaluation_index must; be None, have type int or have the same length as x." + ) + values = self.problem.evaluate(x) if evaluation_index is None: @@ -128,7 +282,5 @@ def evaluate( if isinstance(evaluation_index, int): return values[:, evaluation_index] - assert len(x) == len(evaluation_index), "evaluation_index should be the same length as data" - evaluation_index = np.array(evaluation_index, dtype=np.int32) return values[np.arange(len(evaluation_index)), evaluation_index] diff --git a/vectoptal/ordering_cone.py b/vectoptal/ordering_cone.py index e6bbb1b..50b10c9 100644 --- a/vectoptal/ordering_cone.py +++ b/vectoptal/ordering_cone.py @@ -11,7 +11,7 @@ class OrderingCone: r""" Represents a polyhedral ordering cone in the form :math:`C := \{ x | \mathbf{W}x \geq 0 \}`, - where :math`\mathbf{W}` is a matrix defining the cone's boundaries. The ordering cone is + where :math:`\mathbf{W}` is a matrix defining the cone's boundaries. The ordering cone is used to check if points lie within the cone by testing if they satisfy the inequality. :param W: A 2D array (matrix) that defines the ordering cone. The shape of