diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a46025..f777954 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.7.12] +### Added +- More informative docstrings for optimizer.bayesian, optimizer.predict, to explain choices of surrogate models and aq_funcs + +### Modified +- Renamed aq_func hyperparmeter "Xi_f" to "inflate" +- Moved default aq_func choices for single/multi into aq_defaults of acquisition.config + ## [0.7.11] ### Added - Documentation improvements diff --git a/obsidian/__init__.py b/obsidian/__init__.py index 40bd1bb..21bdaaf 100644 --- a/obsidian/__init__.py +++ b/obsidian/__init__.py @@ -1,5 +1,5 @@ """obsidian: Automated experiment design and black-box optimization""" -__version__ = '0.7.11' +__version__ = '0.7.12' from obsidian.campaign import Campaign from obsidian.optimizer import BayesianOptimizer diff --git a/obsidian/acquisition/config.py b/obsidian/acquisition/config.py index de77364..4665757 100644 --- a/obsidian/acquisition/config.py +++ b/obsidian/acquisition/config.py @@ -28,13 +28,14 @@ universal_aqs = ['RS', 'Mean', 'SF'] valid_aqs = {'single': ['EI', 'NEI', 'PI', 'UCB', 'SR', 'NIPV'] + universal_aqs, 'multi': ['EHVI', 'NEHVI', 'NParEGO'] + universal_aqs} +aq_defaults = {'single': 'NEI', 'multi': 'NEHVI'} # For default values, 'optional' indicates whether or not a default value (or None) 'val' can be used aq_hp_defaults = { - 'EI': {'Xi_f': {'val': 0, 'optional': True}}, + 'EI': {'inflate': {'val': 0, 'optional': True}}, 'NEI': {}, - 'PI': {'Xi_f': {'val': 0, 'optional': True}}, + 'PI': {'inflate': {'val': 0, 'optional': True}}, 'UCB': {'beta': {'val': 1, 'optional': True}}, 'SR': {}, 'RS': {}, diff --git a/obsidian/acquisition/custom.py b/obsidian/acquisition/custom.py index 56782cb..4f41b6a 100644 --- a/obsidian/acquisition/custom.py +++ b/obsidian/acquisition/custom.py @@ -40,6 +40,9 @@ def __init__(self, @t_batch_mode_transform() def forward(self, x: Tensor) -> Tensor: + """ + Evaluate the acquisition function on the candidate set x + """ # x dimensions: b * q * d posterior = self.model.posterior(x) # dimensions: b * q samples = self.get_posterior_samples(posterior) # dimensions: n * b * q * o @@ -81,6 +84,9 @@ def __init__(self, @t_batch_mode_transform() def forward(self, x: Tensor) -> Tensor: + """ + Evaluate the acquisition function on the candidate set x + """ # x dimensions: b * q * d x_train = self.model.train_inputs[0][0] # train_inputs is a list of tuples diff --git a/obsidian/campaign/analysis.py b/obsidian/campaign/analysis.py index 4138ef4..c894d9a 100644 --- a/obsidian/campaign/analysis.py +++ b/obsidian/campaign/analysis.py @@ -74,7 +74,7 @@ def plot_interactions(optimizer, cor, clamp=False): optimizer (BayesianOptimizer): The optimizer object which contains a surrogate that has been fit to data and can be used to make predictions. cor (np.ndarray): The correlation matrix representing the parameter interactions. - clamp (bool, optional): Whether to clamp the colorbar range to (0, 1). Defaults to False. + clamp (bool, optional): Whether to clamp the colorbar range to (0, 1). Defaults to ``False``. Returns: matplotlib.figure.Figure: The parameter interaction plot @@ -112,10 +112,10 @@ def calc_ofat_ranges(optimizer, threshold, X_ref, PI_range=0.7, threshold (float): The response value threshold (minimum value) which would be considered passing for OFAT variations. PI_range (float, optional): The prediction interval coverage (fraction of density) steps (int, optional): The number of steps to use in the search for the OFAT boundaries. - The default value is 100. + The default value is ``100``. response_id (int, optional): The index of the relevant response within the fitted optimizer object. - The default value is 0. - calc_interacts (bool, optional): Whether or not to return the interaction matrix; default is True. + The default value is ``0``. + calc_interacts (bool, optional): Whether or not to return the interaction matrix; default is ``True``. Returns: ofat_ranges (pd.DataFrame): A dataframe describing the min/max OFAT values using each LB, UB, and average prediction. @@ -206,9 +206,9 @@ def sensitivity(optimizer, Args: optimizer (BayesianOptimizer): The optimizer object which contains a surrogate that has been fit to data and can be used to make predictions. - dx (float, optional): The perturbation size for calculating the sensitivity. Defaults to 1e-6. + dx (float, optional): The perturbation size for calculating the sensitivity. Defaults to ``1e-6``. X_ref (pd.DataFrame | None, optional): The reference input values for calculating the sensitivity. - If None, the mean of X_space will be used as the reference. Defaults to None. + If None, the mean of X_space will be used as the reference. Defaults to ``None``. Returns: pd.DataFrame: A DataFrame containing the sensitivity values for each parameter in X_space. diff --git a/obsidian/campaign/campaign.py b/obsidian/campaign/campaign.py index e7a0dfe..2ae7198 100644 --- a/obsidian/campaign/campaign.py +++ b/obsidian/campaign/campaign.py @@ -89,37 +89,45 @@ def add_data(self, df: pd.DataFrame): self.data.index.name = 'Observation ID' def clear_data(self): + """Clears campaign data""" self.data = None @property def optimizer(self) -> Optimizer: + """Campaign Optimizer""" return self._optimizer def set_optimizer(self, optimizer: Optimizer): + """Sets the campaign optimizer""" self._optimizer = optimizer @property def designer(self) -> ExpDesigner: + """Campaign Experimental Designer""" return self._designer def set_designer(self, designer: ExpDesigner): + """Sets the campaign experiment designer""" self._designer = designer @property def objective(self) -> Objective | None: + """Campaign Objective function""" return self._objective def set_objective(self, objective: Objective | None): + """Sets the campaign objective function""" self._objective = objective @property def target(self): + """Campaign experimental target(s)""" return self._target def set_target(self, target: Target | list[Target]): """ - Sets the target for the campaign. + Sets the experimental target context for the campaign. Args: target (Target | list[Target] | None): The target or list of targets to set. @@ -243,6 +251,7 @@ def load_state(cls, return new_campaign def __repr__(self): + """String representation of object""" return f"obsidian Campaign for {getattr(self,'y_names', None)}; {getattr(self,'m_exp', 0)} observations" def initialize(self, diff --git a/obsidian/campaign/explainer.py b/obsidian/campaign/explainer.py index 045a658..c37ba0c 100644 --- a/obsidian/campaign/explainer.py +++ b/obsidian/campaign/explainer.py @@ -50,9 +50,11 @@ def __repr__(self): @property def optimizer(self): + """Explainer Optimizer object""" return self._optimizer def set_optimizer(self, optimizer: Optimizer): + """Sets the explainer optimizer""" self._optimizer = optimizer def shap_explain(self, @@ -114,6 +116,7 @@ def f_preds(X): return def shap_summary(self) -> Figure: + """SHAP Summary Plot (Beeswarm)""" if not self.shap: raise ValueError('shap explainer is not fit.') @@ -121,6 +124,7 @@ def shap_summary(self) -> Figure: return fig def shap_summary_bar(self) -> Figure: + """SHAP Summary Plot (Bar Plot / Importance)""" if not self.shap: raise ValueError('shap explainer is not fit.') @@ -136,7 +140,7 @@ def shap_pdp_ice(self, ace_opacity: float = 0.5, ace_linewidth="auto" ) -> Figure: - + """SHAP Partial Dependence Plot with ICE""" if not self.shap: raise ValueError('shap explainer is not fit.') @@ -156,6 +160,7 @@ def shap_pdp_ice(self, def shap_single_point(self, X_new, X_ref=None): + """SHAP Pair-wise Marginal Explanations""" if not self.shap: raise ValueError('shap explainer is not fit.') @@ -181,7 +186,7 @@ def shap_single_point(self, def cal_sensitivity(self, dx: float = 1e-6, X_ref: pd.DataFrame | None = None) -> pd.DataFrame: - + """Local parameter sensitivity analysis""" df_sens = sensitivity(self.optimizer, dx=dx, X_ref=X_ref) return df_sens diff --git a/obsidian/constraints/input.py b/obsidian/constraints/input.py index 809ee5b..3c36557 100644 --- a/obsidian/constraints/input.py +++ b/obsidian/constraints/input.py @@ -31,9 +31,9 @@ def InConstraint_Generic(X_space: ParamSpace, Args: X_space (ParamSpace): The parameter space object. - indices (list[float | int], optional): The indices of the parameters to be constrained. Defaults to [0]. - coeff (list[float | int | list], optional): The coefficients for the parameters in the constraint equation. Defaults to [0]. - rhs (int | float, optional): The right-hand side value of the constraint equation. Defaults to -1. + indices (list[float | int], optional): The indices of the parameters to be constrained. Defaults to ``[0]``. + coeff (list[float | int | list], optional): The coefficients for the parameters in the constraint equation. Defaults to ``[0]``. + rhs (int | float, optional): The right-hand side value of the constraint equation. Defaults to ``-1``. Returns: tuple[Tensor, Tensor, Tensor]: A tuple containing the indices, coefficients, and right-hand side value of the constraint. @@ -102,7 +102,7 @@ def InConstraint_ConstantDim(X_space: ParamSpace, Args: X_space (ParamSpace): The parameter space object. dim (int): The dimension of the constant parameter. - tol (int | float, optional): The tolerance value. Defaults to 0.01. + tol (int | float, optional): The tolerance value. Defaults to ``0.01``. Returns: tuple[callable, bool]: A tuple containing the constraint function and a boolean indicating if it is an inter-point constraint. diff --git a/obsidian/constraints/output.py b/obsidian/constraints/output.py index e1048ca..dfc624c 100644 --- a/obsidian/constraints/output.py +++ b/obsidian/constraints/output.py @@ -5,12 +5,13 @@ import torch from torch import Tensor +from typing import Callable # Negative values imply feasibility! # Note that these are OUTPUT constraints -def OutConstraint_Blank(target: Target | list[Target]) -> callable: +def OutConstraint_Blank(target: Target | list[Target]) -> Callable: """ Dummy constraint function that proposes all samples as feasible. @@ -29,7 +30,7 @@ def constraint(samples: Tensor) -> Tensor: def OutConstraint_L1(target: Target | list[Target], - offset: int | float = 1) -> callable: + offset: int | float = 1) -> Callable: """ Calculates the L1 (absolute-value penalized) constraint diff --git a/obsidian/experiment/design.py b/obsidian/experiment/design.py index 9189427..dae3758 100644 --- a/obsidian/experiment/design.py +++ b/obsidian/experiment/design.py @@ -37,6 +37,7 @@ def __init__(self, self.seed = seed def __repr__(self): + """String representation of object""" return f"obsidian ExpDesigner(X_space={self.X_space})" def initialize(self, @@ -48,9 +49,9 @@ def initialize(self, Args: m_initial (int): The number of experiments to initialize. - method (str, optional): The method to use for initialization. Defaults to 'LHS'. - seed (int | None, optional): The randomization seed. Defaults to None. - sample_custom (Tensor | ArrayLike | None, optional): Custom samples for initialization. Defaults to None. + method (str, optional): The method to use for initialization. Defaults to ``'LHS'``. + seed (int | None, optional): The randomization seed. Defaults to ``None``. + sample_custom (Tensor | ArrayLike | None, optional): Custom samples for initialization. Defaults to ``None``. Returns: pd.DataFrame: The initialized experiment design. diff --git a/obsidian/experiment/simulator.py b/obsidian/experiment/simulator.py index 2738dbd..b8dc4ff 100644 --- a/obsidian/experiment/simulator.py +++ b/obsidian/experiment/simulator.py @@ -18,8 +18,9 @@ class Simulator: Attributes: X_space (ParamSpace): The ParamSpace object representing the allowable space for optimization. response_function (Callable): The callable function used to convert experiments to responses. - name (str or list[str]): Name of the simulated output(s). - eps (float or list[float]): The simulated error to apply, as the standard deviation of the Standard Normal distribution. + name (str or list[str]): Name of the simulated output(s). Default is ``Response``. + eps (float or list[float]): The simulated error to apply, as the standard deviation of the Standard + Normal distribution. Default is ``0``. kwargs (dict): Optional hyperparameters for the response function. Raises: @@ -47,6 +48,7 @@ def __init__(self, self.kwargs = kwargs def __repr__(self): + """String representation of object""" return f" obsidian Simulator(response_function={self.response_function.__name__}, eps={self.eps})" def simulate(self, diff --git a/obsidian/experiment/utils.py b/obsidian/experiment/utils.py index 12ae012..b9d068b 100644 --- a/obsidian/experiment/utils.py +++ b/obsidian/experiment/utils.py @@ -19,11 +19,11 @@ def factorial_DOE(d: int, Args: d (int): Number of dimensions/inputs in the design. n_CP (int, optional): The number of centerpoints to include in the design, for estimating - uncertainty and curvature. Default is 3. + uncertainty and curvature. Default is ``3``. shuffle (bool, optional): Whether or not to shuffle the design or leave them in the default run - order. Default is True. - seed (int, optional): Randomization seed. Default is None. - full (bool, optional): Whether or not to run the full DOE. Default is False, which + order. Default is ``True``. + seed (int, optional): Randomization seed. Default is ``None``. + full (bool, optional): Whether or not to run the full DOE. Default is ``False``, which will lead to an efficient Res4+ design. Returns: diff --git a/obsidian/objectives/base.py b/obsidian/objectives/base.py index 90c5b93..4854393 100644 --- a/obsidian/objectives/base.py +++ b/obsidian/objectives/base.py @@ -26,6 +26,7 @@ def __init__(self, def output_shape(self, samples: Tensor) -> Tensor: + """Converts the output to the correct Torch shape based on the multi-output flag""" return samples.squeeze(-1) if not self._is_mo else samples @abstractmethod @@ -36,7 +37,7 @@ def forward(self, pass # pragma: no cover def save_state(self) -> dict: - + """Saves the objective to a state dictionary""" obj_dict = {'name': self.__class__.__name__, 'state_dict': tensordict_to_dict(self.state_dict())} @@ -44,8 +45,9 @@ def save_state(self) -> dict: @classmethod def load_state(cls, obj_dict: dict): - + """Loads the objective from a state dictionary""" return cls(**obj_dict['state_dict']) def __repr__(self): + """String representation of object""" return f'{self.__class__.__name__} (mo={self._is_mo})' diff --git a/obsidian/objectives/config.py b/obsidian/objectives/config.py index fc591e3..7e9c926 100644 --- a/obsidian/objectives/config.py +++ b/obsidian/objectives/config.py @@ -1,3 +1,5 @@ +"""Method pointers and config for objective functions""" + from .custom import ( Identity_Objective, Feature_Objective, diff --git a/obsidian/objectives/custom.py b/obsidian/objectives/custom.py index 8da81e7..270d00f 100644 --- a/obsidian/objectives/custom.py +++ b/obsidian/objectives/custom.py @@ -21,7 +21,9 @@ def __init__(self, def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: - + """ + Evaluate the objective function on the candidate set samples, X + """ return self.output_shape(samples) @@ -34,9 +36,9 @@ class Feature_Objective(Objective): Args: X_space (ParamSpace): The parameter space. indices (list[float | int], optional): The indices of the parameters in the real space - to be used as features. Defaults to [0]. + to be used as features. Defaults to ``[0]``. coeff (list[float | int | list], optional): The coefficients corresponding to each feature. - Defaults to [0]. + Defaults to ``[0]``. Raises: ValueError: If the length of `indices` and `coeff` are not the same. @@ -62,11 +64,15 @@ def __init__(self, self.X_space = X_space def __repr__(self): + """String representation of object""" return f'{self.__class__.__name__} (indices={self.indices.tolist()}, coeff={self.coeff.tolist()})' def forward(self, samples: Tensor, X: Tensor) -> Tensor: + """ + Evaluate the objective function on the candidate set samples, X + """ # Ydim = s * b * q * m # Xdim = b * q * d # if q is 1, it is omitted @@ -89,7 +95,7 @@ def forward(self, return total_obj def save_state(self) -> dict: - + """Saves the objective to a state dictionary""" obj_dict = {'name': self.__class__.__name__, 'state_dict': tensordict_to_dict(self.state_dict()), 'X_space': self.X_space.save_state()} @@ -98,7 +104,7 @@ def save_state(self) -> dict: @classmethod def load_state(cls, obj_dict: dict): - + """Loads the objective from a state dictionary""" new_obj = cls(ParamSpace.load_state(obj_dict['X_space']), **obj_dict['state_dict']) @@ -148,16 +154,19 @@ def __init__(self, def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: - + """ + Evaluate the objective function on the candidate set samples, X + """ distance = (-1)*(self.u_t-samples).abs() return self.output_shape(distance) def __repr__(self): + """String representation of object""" return f'{self.__class__.__name__} (utopian={self.utopian.tolist()})' def save_state(self) -> dict: - + """Saves the objective to a state dictionary""" obj_dict = {'name': self.__class__.__name__, 'state_dict': tensordict_to_dict(self.state_dict()), 'targets': [t.save_state() for t in self.targets]} @@ -166,7 +175,7 @@ def save_state(self) -> dict: @classmethod def load_state(cls, obj_dict: dict): - + """Loads the objective from a state dictionary""" new_obj = cls(targets=[Target.load_state(t_dict) for t_dict in obj_dict['targets']], **obj_dict['state_dict']) @@ -183,7 +192,7 @@ class Bounded_Target(Objective): If a bound is None, it is ignored. targets (Target | list[Target]): The target or list of targets. tau (float, optional): The temperature parameter for the sigmoid function. - Defaults to 1e-3. + Defaults to ``1e-3``. Attributes: lb (torch.Tensor): The lower bounds for the targets. @@ -229,12 +238,15 @@ def __init__(self, self.register_buffer('tau', tau) def __repr__(self): + """String representation of object""" return f'{self.__class__.__name__} (indices={self.indices.tolist()}, bounds={self.bounds})' def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: - + """ + Evaluate the objective function on the candidate set samples, X + """ approx_lb = torch.sigmoid((samples[..., self.indices] - self.lb) / self.tau) approx_ub = torch.sigmoid((self.ub - samples[..., self.indices]) / self.tau) product = approx_lb * approx_ub @@ -243,7 +255,7 @@ def forward(self, return self.output_shape(out) def save_state(self) -> dict: - + """Saves the objective to a state dictionary""" obj_dict = {'name': self.__class__.__name__, 'state_dict': tensordict_to_dict(self.state_dict()), 'bounds': self.bounds, @@ -253,7 +265,7 @@ def save_state(self) -> dict: @classmethod def load_state(cls, obj_dict: dict): - + """Loads the objective from a state dictionary""" new_obj = cls(targets=[Target.load_state(t_dict) for t_dict in obj_dict['targets']], bounds=obj_dict['bounds'], **obj_dict['state_dict']) @@ -277,9 +289,13 @@ def __init__(self, self.register_buffer('index', torch.tensor(index)) def __repr__(self): + """String representation of object""" return f'{self.__class__.__name__} (index={self.index.item()})' def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: + """ + Evaluate the objective function on the candidate set samples, X + """ return samples[..., self.index] diff --git a/obsidian/objectives/scalarize.py b/obsidian/objectives/scalarize.py index 5106bde..9983aa7 100644 --- a/obsidian/objectives/scalarize.py +++ b/obsidian/objectives/scalarize.py @@ -21,6 +21,7 @@ def __init__(self) -> None: super().__init__(mo=False) def __repr__(self): + """String representation of object""" return f'{self.__class__.__name__} (weights={self.weights.tolist()})' @@ -40,7 +41,9 @@ def __init__(self, def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: - + """ + Evaluate the objective function on the candidate set samples, X + """ return (self.weights * samples).sum(dim=-1) @@ -73,7 +76,9 @@ def __init__(self, def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: - + """ + Evaluate the objective function on the candidate set samples, X + """ return self.C*(self.weights * samples).norm(self.norm, dim=-1) @@ -85,8 +90,8 @@ class Scalar_Chebyshev(Scalarization): Args: weights (list[float]): A list of weights to be applied to the response tensor. - alpha (float, optional): The scaling factor for the weighted sum. Defaults to 0.05. - augment (bool, optional): Flag indicating whether to perform augmentation. Defaults to True. + alpha (float, optional): The scaling factor for the weighted sum. Defaults to ``0.05``. + augment (bool, optional): Flag indicating whether to perform augmentation. Defaults to ``True``. """ def __init__(self, @@ -101,7 +106,9 @@ def __init__(self, def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: - + """ + Evaluate the objective function on the candidate set samples, X + """ # Augmentation seeks to maximize weighted sum delta = self.alpha * (self.weights * samples).sum(dim=-1) if self.augment else 0 diff --git a/obsidian/objectives/sequence.py b/obsidian/objectives/sequence.py index 169fb3a..5dec107 100644 --- a/obsidian/objectives/sequence.py +++ b/obsidian/objectives/sequence.py @@ -34,19 +34,22 @@ def __init__(self, obj._is_mo = True def __repr__(self): + """String representation of object""" return f'{self.__class__.__name__} (obj_list={self.obj_list})' def forward(self, samples: Tensor, X: Tensor | None = None) -> Tensor: - + """ + Evaluate the objective function(s) on the candidate set samples, X + """ for obj in self.obj_list: samples = obj.forward(samples, X=X) return samples def save_state(self) -> dict: - + """Saves the objective(s) to a state dictionary""" obj_dict = {'name': self.__class__.__name__, 'obj_list': [obj.save_state() for obj in self.obj_list]} @@ -54,7 +57,7 @@ def save_state(self) -> dict: @classmethod def load_state(cls, obj_dict: dict): - + """Loads the objective(s) from a state dictionary""" new_obj_list = [] for obj_dict_i in obj_dict['obj_list']: diff --git a/obsidian/optimizer/base.py b/obsidian/optimizer/base.py index 2fbe033..f4b5d73 100644 --- a/obsidian/optimizer/base.py +++ b/obsidian/optimizer/base.py @@ -128,8 +128,8 @@ def hypervolume(self, Args: f (Tensor): The data points to calculate the hypervolume for. - ref_point (list, optional): The reference point for the hypervolume calculation. Defaults to None. - weights (list, optional): The weights to apply to each objective. Defaults to None. + ref_point (list, optional): The reference point for the hypervolume calculation. Defaults to ``None``. + weights (list, optional): The weights to apply to each objective. Defaults to ``None``. Returns: float: The hypervolume value. @@ -210,6 +210,7 @@ def pf_distance(self, def fit(self, Z: pd.DataFrame, target: Target | list[Target]): + """Fit the optimizer's surrogate models to data""" pass # pragma: no cover @abstractmethod @@ -217,22 +218,27 @@ def predict(self, X: pd.DataFrame, return_f_inv: bool = True, PI_range: float = 0.7): + """Predict the optimizer's target(s) at the candidate set X""" pass # pragma: no cover @abstractmethod def suggest(self): + """Suggest the next optimal experiment(s)""" pass # pragma: no cover @abstractmethod def maximize(self): + """Maximize the optimizer's target(s)""" pass # pragma: no cover @abstractmethod def save_state(self): + """Save the optimizer to a state dictionary""" pass # pragma: no cover @classmethod @abstractmethod def load_state(cls, obj_dict: dict): + """Load the optimizer from a state dictionary""" pass # pragma: no cover diff --git a/obsidian/optimizer/bayesian.py b/obsidian/optimizer/bayesian.py index 1f8c6c6..7f8dfb0 100644 --- a/obsidian/optimizer/bayesian.py +++ b/obsidian/optimizer/bayesian.py @@ -4,7 +4,7 @@ from obsidian.parameters import ParamSpace, Target, Task from obsidian.surrogates import SurrogateBoTorch, DNN -from obsidian.acquisition import aq_class_dict, aq_hp_defaults, valid_aqs +from obsidian.acquisition import aq_class_dict, aq_defaults, aq_hp_defaults, valid_aqs from obsidian.surrogates import model_class_dict from obsidian.objectives import Index_Objective, Objective_Sequence from obsidian.exceptions import IncompatibleObjectiveError, UnsupportedError, UnfitError, DataWarning @@ -41,9 +41,25 @@ class BayesianOptimizer(Optimizer): X_space (ParamSpace): The parameter space defining the search space for the optimization. surrogate (str | dict | list[str] | list[dict], optional): The surrogate model(s) to use. It can be a string representing a single model type, a dictionary specifying multiple model types - with their hyperparameters, or a list of strings or dictionaries. Defaults to ['GP']. - seed (int | None, optional): The random seed to use. Defaults to None. - verbose (int, optional): The verbosity level. Defaults to 1. + with their hyperparameters, or a list of strings or dictionaries. + + Defaults to ``'GP'``. Options are as follows: + + - ``'GP'``: Gaussian Process with default settings (Matern Kernel, Gamma covariance priors) + - ``'MixedGP'``: GP with mixed parameter types (continuous, categorical). Will be re-selected + by default if 'GP' is selected and input space is mixed. + - ``'DKL'``: GP with a NN feature-extractor (deep kernel learning) + - ``'GPflat'``: GP without priors. May result in optimization instability, but removes bias + for special situations. + - ``'GPprior'``: GP with custom priors on the mean, likelihood, and covariance + - ``'MTGP'``: Multi-task GP for multi-output optimization. Will be re-selected by default + if 'GP' is selected and the input space contains Task parameters. + - ``'DNN'``: Dropout neural network. Uses MC sampling to mask neurons during training and + to estimate uncertainty. + + + seed (int | None, optional): The random seed to use. Defaults to ``None``. + verbose (int, optional): The verbosity level. Defaults to ``1``. Attributes: surrogate_type (list[str]): The shorthand name of each surrogate model. @@ -60,7 +76,7 @@ class BayesianOptimizer(Optimizer): """ def __init__(self, X_space: ParamSpace, - surrogate: str | dict | list[str] | list[dict] = ['GP'], + surrogate: str | dict | list[str] | list[dict] = 'GP', seed: int | None = None, verbose: int = 1): @@ -131,7 +147,7 @@ def _validate_target(self, Args: target (Target | list[Target] | None, optional): The target object or a list of target objects to be validated. If None, the target object specified during the initialization of the optimizer will be used. - Defaults to None. + Defaults to ``None``. Raises: TypeError: If the target is not a Target object or a list of Target objects. @@ -335,7 +351,7 @@ def predict(self, Args: X (pd.DataFrame): Experiments to predict over. return_f_inv (bool, optional): Whether or not to return the inverse-transformed objective function, - which is the raw response (unscored). The default is True. Most internal calls set to False to handle + which is the raw response (unscored). The default is ``True``. Most internal calls set to ``False`` to handle the transformed objective function. PI_range (float, optional): The nominal coverage range for the returned prediction interval @@ -468,7 +484,7 @@ def _parse_aq_kwargs(self, X_t_pending (Tensor, optional): Suggested points yet to be run objective (GenericMCMultiOutputObjective or GenericMCObjective, optional): The objective used foroptimization after calling the target models. - Default is `None`. + Default is ``None``. Returns: dict: The parsed acquisition function keyword arguments. @@ -498,7 +514,7 @@ def _parse_aq_kwargs(self, # Improvement aqs based on inflation or deflation of best point if aq in ['EI', 'PI']: - o_max = o.max(dim=0).values * (1+hps['Xi_f']) + o_max = o.max(dim=0).values * (1+hps['inflate']) aq_kwargs.update({'best_f': o_max}) # UCB based on: mu + sqrt(beta) * sqrt(variance) = mu + sqrt(beta) * sd @@ -540,10 +556,10 @@ def _parse_aq_kwargs(self, def suggest(self, m_batch: int = 1, target: Target | list[Target] | None = None, + acquisition: list[str] | list[dict] = None, optim_sequential: bool = True, optim_samples: int = 512, optim_restarts: int = 10, - acquisition: list[str] | list[dict] = None, objective: MCAcquisitionObjective | None = None, out_constraints: list[Callable] | None = None, eq_constraints: tuple[Tensor, Tensor, float] | None = None, @@ -558,32 +574,57 @@ def suggest(self, function calculated from the expectation of a surrogate model. Args: - m_batch (int, optional): The number of experiments to suggest at once. The default is `1`. + m_batch (int, optional): The number of experiments to suggest at once. The default is ``1``. target (Target or list of Target, optional): The response(s) to be used for optimization, + acquisition (list of str or list of dict, optional): Indicator for the desired acquisition function(s). + A list will propose experiments for each acquisition function based on ``optim_sequential``. + + The default is ``['NEI']`` for single-output and ``['NEHVI']`` for multi-output. + Options are as follows: + + - ``'EI'``: Expected Improvement (relative to best of ``y_train``). Accepts hyperparameter + ``'inflate'``, a positive or negative float to inflate/deflate the best point for explore/exploit. + - ``'NEI'``: Noisy Expected Improvement. More robust than ``EI`` and uses all of ``y_train``, + but accepts no hyperparameters. + - ``'PI'``: Probability of Improvement (relative to best of ``y_train``). Accepts hyperparameter + ``'inflate'``, a positive or negative float to inflate/deflate the best point for explore/exploit. + - ``'UCB'``: Upper Confidence Bound. Accepts hyperparameter ``'beta'``, a positive float which sets + the number of standard deviations above the mean. + - ``'SR'``: Simple Regret + - ``'RS'``: Random Sampling + - ``'Mean'``: Mean of the posterior distribution (pure exploitation/maximization of objective) + - ``'SF'``: Space Filling. Requests points that maximize the minimumd distance to ``X_train`` based + on Euclidean distance. + - ``'NIPV'``: Negative Integrated Posterior Variance. Requests the point which most improves the prediction + interval for a random selection of points in the design space. Used for active learning. + - ``'EHVI'``: Expected Hypervolume Improvement. Can accept a ``ref_point``, otherwise a point just + below the minimum of ``y_train``. + - ``'NEHVI'``: Noisy Expected Hypervolume Improvement. Can accept a ``ref_point``, otherwise a point + just below the minimum of ``y_train``. + - ``'NParEGO'``: Noisy Pareto Efficient Global Optimization. Can accept ``scalarization_weights``, a + list of weights for each objective. + optim_sequential (bool, optional): Whether or not to optimize batch designs sequentially - (by fantasy) or simultaneously. Default is `True`. + (by fantasy) or simultaneously. Default is ``True``. optim_samples (int, optional): The number of samples to use for quasi Monte Carlo sampling of the acquisition function. Also used for initializing the acquisition optimizer. - The default value is `512`. + The default value is ``512``. optim_restarts (int, optional): The number of restarts to use in the global optimization - of the acquisition function. The default value is `10`. - acquisition (list of str or list of dict, optional): Indicator for the desired acquisition function(s). - The default is `['EI']`. A list will propose experiments for each acquisition function - in parallel and independent. + of the acquisition function. The default value is ``10``. objective (MCAcquisitionObjective, optional): The objective function to be used for optimization. - The default is `None`. + The default is ``None``. out_constraints (list of Callable, optional): A list of constraints to be applied to the output space. - The default is `None`. + The default is ``None``. eq_constraints (tuple of Tensor, Tensor, float, optional): A tuple of tensors representing the equality - constraints, the target values, and the tolerance. The default is `None`. + constraints, the target values, and the tolerance. The default is ``None``. ineq_constraints (tuple of Tensor, Tensor, float, optional): A tuple of tensors representing the inequality - constraints, the target values, and the tolerance. The default is `None`. + constraints, the target values, and the tolerance. The default is ``None``. nleq_constraints (tuple of Callable, bool, optional): A tuple of functions representing the nonlinear inequality constraints and a boolean indicating whether the constraints are active. - The default is `None`. - task_index (int, optional): The index of the task to optimize for multi-task models. The default is `0`. + The default is ``None``. + task_index (int, optional): The index of the task to optimize for multi-task models. The default is ``0``. fixed_var (dict(str:float), optional): Name of a variable and setting, over which the - suggestion should be fixed. Default values is `None` + suggestion should be fixed. Default values is ``None`` X_pending (pd.DataFrame, optional): Experiments that are expected to be run before the next optimal set eval_pending (pd.DataFrame, optional): Acquisition values associated with X_pending @@ -634,7 +675,7 @@ def suggest(self, # Default to noisy expected improvement if no aq method is provided if not acquisition: - acquisition = ['NEI'] if optim_type == 'single' else ['NEHVI'] + acquisition = [aq_defaults[optim_type]] if not isinstance(acquisition, list): raise TypeError('acquisition must be a list of strings or dictionaries') @@ -794,8 +835,8 @@ def evaluate(self, acquisition (str | dict, optional): Acquisition function name (str) or dictionary containing the acquisition function name and its hyperparameters. objective (MCAcquisitionObjective, optional): The objective function to be used for optimization. - The default is `None`. - eval_aq (bool, optional): Whether or not to also evaluate the aq function. The default is `False`. + The default is ``None``. + eval_aq (bool, optional): Whether or not to also evaluate the aq function. The default is ``False``. Returns: pd.DataFrame: Response prediction, pred interval, transformed mean, aq value, @@ -873,7 +914,7 @@ def evaluate(self, if eval_aq: # Default to noisy expected improvement if no aq method is provided if not acquisition: - acquisition = 'NEI' if optim_type == 'single' else 'NEHVI' + acquisition = [aq_defaults[optim_type]] if not isinstance(acquisition, (str, dict)): raise TypeError('Acquisition must be either a string or a dictionary') @@ -941,10 +982,10 @@ def maximize(self, Predicts the conditions which return the maximum response value within the parameter space. Args: - optim_samples (int): The number of samples to be used for optimization. Default is 1026. - optim_restarts (int): The number of restarts for the optimization process. Default is 50. + optim_samples (int): The number of samples to be used for optimization. Default is ``1026``. + optim_restarts (int): The number of restarts for the optimization process. Default is ``50``. fixed_var (dict(str:float), optional): Name of a variable and setting, over which the - suggestion should be fixed. Default values is `None` + suggestion should be fixed. Default values is ``None`` Returns: tuple[pd.DataFrame, pd.DataFrame] = (X_suggest, eval_suggest) X_suggest (pd.DataFrame): Experiment matrix of real input variables, diff --git a/obsidian/parameters/base.py b/obsidian/parameters/base.py index 7bb428c..3cbc2d0 100644 --- a/obsidian/parameters/base.py +++ b/obsidian/parameters/base.py @@ -18,14 +18,17 @@ def __repr__(self): @abstractmethod def _validate_value(self, value: int | float | str): + """Validate data inputs""" pass # pragma: no cover @abstractmethod def encode(X): + """Encode parameter to a format that can be used for training""" pass # pragma: no cover @abstractmethod def decode(X): + """Decode parameter from transformed space""" pass # pragma: no cover def save_state(self) -> dict: diff --git a/obsidian/parameters/continuous.py b/obsidian/parameters/continuous.py index baffdb1..f8b4b56 100644 --- a/obsidian/parameters/continuous.py +++ b/obsidian/parameters/continuous.py @@ -23,10 +23,12 @@ class Param_Continuous(Parameter): @transform_with_type def unit_map(self, X: np.ndarray): + """Map from measured to 0,1 space""" return (X-self.min)/self.range if self.range != 0 else 0*X @transform_with_type def unit_demap(self, X: np.ndarray): + """Map from 0,1 to measured space""" return X*self.range+self.min encode = unit_map @@ -35,9 +37,11 @@ def unit_demap(self, X: np.ndarray): @property def range(self): + """The range of the parameter (max - min)""" return self.max-self.min def __repr__(self): + """String representation of object""" return f"{self.__class__.__name__}(name={self.name}, min={self.min}, max={self.max})" def _validate_value(self, value: int | float): diff --git a/obsidian/parameters/discrete.py b/obsidian/parameters/discrete.py index 460a1d5..7737093 100644 --- a/obsidian/parameters/discrete.py +++ b/obsidian/parameters/discrete.py @@ -58,14 +58,17 @@ def unit_demap(self, X: np.ndarray): @property def min(self): + """Minimum parameter value (always 0 for discrete)""" return 0 @property def nc(self): + """Number of discrete categories""" return len(self.categories) @property def max(self): + """Maximum parameter value (nc-1)""" return self.nc-1 def _validate_value(self, @@ -98,6 +101,7 @@ def __init__(self, self._validate_value(c) def __repr__(self): + """String representation of object""" return f"{self.__class__.__name__}(name={self.name}, categories={self.categories})" @@ -108,9 +112,11 @@ class Param_Ordinal(Param_Discrete): # Ordinal encoder maps to integers def encode(self, X: np.ndarray): + """Encode parameter to a format that can be used for training""" return self.unit_map(X) def decode(self, X: np.ndarray): + """Decode parameter from transformed space""" return self.unit_demap(X) @@ -122,7 +128,7 @@ class Param_Categorical(Param_Discrete): # Categorical encoder maps to one-hot columns # No decorator on this, we need to handle dataframes def encode(self, X: str | ArrayLike): - + """Encode parameter to a format that can be used for training""" X_str = np.array(X).flatten().astype('U11') X_cat = pd.Series(X_str).astype(pd.CategoricalDtype(categories=self.categories)) X_ohe = pd.get_dummies(X_cat, prefix_sep=CAT_SEP, dtype=float, prefix=self.name) @@ -130,7 +136,7 @@ def encode(self, X: str | ArrayLike): return X_ohe def decode(self, X: ArrayLike): - + """Decode parameter from transformed space""" return [self.categories[int(x)] for x in np.array(X).argmax(axis=1)] @@ -142,10 +148,12 @@ class Task(Param_Discrete): @transform_with_type def encode(self, X: np.ndarray): + """Encode parameter to a format that can be used for training""" return self.unit_map(X)*self.nc @transform_with_type def decode(self, X: np.ndarray): + """Decode parameter from transformed space""" return np.array(self.unit_demap(X/self.nc)).astype('U11') @@ -160,14 +168,17 @@ class Param_Discrete_Numeric(Param_Discrete): @property def min(self): + """Minimum parameter value""" return np.array(self.categories).min() @property def max(self): + """Maximum parameter value""" return np.array(self.categories).max() @property def range(self): + """The range of the parameter (max - min)""" return self.max-self.min def __init__(self, @@ -205,10 +216,12 @@ def _validate_value(self, @transform_with_type def unit_map(self, X: np.ndarray): + """Map from measured to 0,1 space""" return (X-self.min)/self.range if self.range != 0 else 0*X @transform_with_type def unit_demap(self, X: np.ndarray): + """Map from 0,1 to measured space""" closest_idx = np.abs(np.array(self.categories) - (X.flatten()[..., np.newaxis]*self.range+self.min)).argmin(axis=1) return np.array(self.categories)[closest_idx].reshape(X.shape) diff --git a/obsidian/parameters/param_space.py b/obsidian/parameters/param_space.py index 29728ae..b4fe07a 100644 --- a/obsidian/parameters/param_space.py +++ b/obsidian/parameters/param_space.py @@ -107,15 +107,19 @@ def __init__(self, return def __iter__(self): + """Iterate over the parameters in the parameter space""" return iter(self.params) def __len__(self): + """Number of parameters in the parameter space""" return len(self.params) def __repr__(self): + """String representation of object""" return f"{self.__class__.__name__}(params={[p.name for p in self]})" def __getitem__(self, index: int) -> Parameter: + """Retrieve a parameter by index""" return self.params[index] def map_transform(self) -> dict: @@ -198,15 +202,19 @@ def _transform(self, # Define transformation methods using a handler method and lambdas; more readable on outer layer def unit_map(self, X): + """Map from measured to 0,1 space""" return self._transform(X, type='unit_map') def unit_demap(self, X): + """Map from 0,1 space to measured space""" return self._transform(X, type='unit_demap') def encode(self, X): + """Encode parameter to a format that can be used for training""" return self._transform(X, type='encode') def decode(self, X): + """Decode parameter from transformed space""" return self._transform(X, type='decode') def save_state(self) -> dict: diff --git a/obsidian/parameters/targets.py b/obsidian/parameters/targets.py index dc23b04..c1e292e 100644 --- a/obsidian/parameters/targets.py +++ b/obsidian/parameters/targets.py @@ -37,6 +37,7 @@ def __init__(self, self.f_transform = f_transform def __repr__(self): + """String representation of object""" return f"{self.__class__.__name__}({self.name}, aim={self.aim})" def transform_f(self, @@ -49,8 +50,8 @@ def transform_f(self, Args: f (array-like): The column(s) containing the response values (y) - inverse (bool, optional): An indicator to perform the inverse transform. Defaults to False. - fit (bool, optional): An indicator to fit the properties of the transform function. Defaults to False. + inverse (bool, optional): An indicator to perform the inverse transform. Defaults to ``False``. + fit (bool, optional): An indicator to fit the properties of the transform function. Defaults to ``False``. Returns: pd.Series: An array of transformed f values matching the responses in Z diff --git a/obsidian/parameters/transforms.py b/obsidian/parameters/transforms.py index 24830c5..edae562 100644 --- a/obsidian/parameters/transforms.py +++ b/obsidian/parameters/transforms.py @@ -36,16 +36,19 @@ def _validate_fit(self): def forward(self, X: Tensor, fit: bool = False): + """Evaluate the forward transformation on input data X""" pass # pragma: no cover @abstractmethod def inverse(self, X: Tensor): + """Inverse transform the transformed data X_t""" pass # pragma: no cover def __call__(self, X: Tensor, fit: bool = False): + """Shortcut to forward method""" return self.forward(X, fit) @@ -56,10 +59,12 @@ class Identity_Scaler(Target_Transform): def forward(self, X: Tensor, fit: bool = False): + """Evaluate the forward transformation on input data X""" return X def inverse(self, X: Tensor): + """Inverse transform the transformed data X_t""" return X @@ -73,7 +78,7 @@ def __init__(self): def forward(self, X: Tensor, fit: bool = False): - + """Evaluate the forward transformation on input data X""" if fit: X_v = X[~X.isnan()] self.params = {'mu': X_v.mean(), 'sd': X_v.std()} @@ -82,6 +87,7 @@ def forward(self, return (X-self.params['mu'])/self.params['sd'] def inverse(self, X): + """Inverse transform the transformed data X_t""" self._validate_fit() return X*self.params['sd']+self.params['mu'] @@ -103,7 +109,7 @@ def __init__(self, def _fit_minmax(self, X: Tensor): - + """Fits the min-max scale of the logit transform""" # Scale X into a range from 0-1 with buffer/2 on either side self.override_fit = False range_response = X.max()-X.min() @@ -114,6 +120,7 @@ def _fit_minmax(self, def forward(self, X: Tensor, fit: bool = False): + """Evaluate the forward transformation on input data X""" # If fit is not called, and the range is valid, transform # If the range is invalid, fit the range first and warn if not fit or self.override_fit: @@ -136,6 +143,7 @@ def forward(self, def inverse(self, X: Tensor): + """Inverse transform the transformed data X_t""" if self.standardize: self._validate_fit() X = X*self.params['sd']+self.params['mu'] diff --git a/obsidian/plotting/plotly.py b/obsidian/plotting/plotly.py index 58fcca3..5df6826 100644 --- a/obsidian/plotting/plotly.py +++ b/obsidian/plotting/plotly.py @@ -23,9 +23,9 @@ def parity_plot(optimizer: Optimizer, and can be used to make predictions. f_transform (bool, optional): An indicator for whether or not to plot the response value in the "objective function" form which is directly used by the optimizer, else using the "measured response" form which the - optimizer preproceses. Default value is `False` which plots the raw measured response scale. + optimizer preproceses. Default value is ``False`` which plots the raw measured response scale. response_id (int, optional): Index of the response for potential multi-response models. - Default value is `0` (single-response). + Default value is ``0`` (single-response). Returns: fig (Figure): The optimizer fit parity plot @@ -99,16 +99,16 @@ def factor_plot(optimizer: Optimizer, optimizer (Optimizer): The optimizer object which contains a surrogate that has been fit to data and can be used to make predictions. feature_id (int, optional): The index of the desired variable to plot from the last data used - to fit the surrogate model. The default value is 0. + to fit the surrogate model. The default value is ``0``. response_id (int, optional): Index of the response for potential multi-response models. - Default value is 0 (single-response). + Default value is ``0`` (single-response). f_transform (bool, optional): An indicator for whether or not to plot the response value in the "objective function" form which is directly used by the optimizer, else using the "measured response" form which the optimizer preproceses. Default value is - False which plots the raw measured response scale. + ``False`` which plots the raw measured response scale. plotRef (bool, optional): An indicator for whether or not to plot the reference data points. - Default value is True. - ylim (tuple, optional): The y-axis limits for the plot. Default value is None. + Default value is ``True``. + ylim (tuple, optional): The y-axis limits for the plot. Default value is ``None``. Returns: fig (Figure): The matplotlib plot of response value versus 1 predictor variable. @@ -199,17 +199,17 @@ def surface_plot(optimizer: Optimizer, optimizer (Optimizer): The optimizer object which contains a surrogate that has been fit to data and can be used to make predictions. feature_ids (list, optional): A list of integers containing the indices of the desired variables - to plot from the last data used to fit the surrogate model. Default value is `[0,1]`. + to plot from the last data used to fit the surrogate model. Default value is ``[0,1]``. f_transform (bool, optional): An indicator for whether or not to plot the response value in the "objective function" form which is directly used by the optimizer, else using the "measured response" form which the optimizer preprocesses. Default value is - `False` which plots the raw measured response scale. + ``False`` which plots the raw measured response scale. plot_bands (bool, optional): An indicator for whether or not to plot the confidence bands as a wire - frame around the surface plot. Default is `True`. + frame around the surface plot. Default is ``True``. plot_data (bool, optional): An indicator for whether or not to plot the raw data locations. - Default is `False`, as the data z-height can be misleading for >2D data on a 3D plot. + Default is ``False``, as the data z-height can be misleading for >2D data on a 3D plot. response_id (int, optional): Index of the response for potential multi-response models. - Default value is `0` (single-response). + Default value is ``0`` (single-response). Returns: fig (Figure): The matplotlib plot of surfaces over a 2-parameter grid. @@ -327,11 +327,11 @@ def MOO_results(campaign: Campaign, Args: campaign (Campaign): The campaign object containing the data. - response_ids (list[int], optional): The indices of the responses to plot. Defaults to [0, 1]. + response_ids (list[int], optional): The indices of the responses to plot. Defaults to ``[0, 1]``. color_feature_id (int | None, optional): The index of the feature to use for coloring the markers. - Defaults to None. + Defaults to ``None``. y_suggest (pd.DataFrame | None, optional): The suggested data for the responses. - Defaults to None. + Defaults to ``None``. Returns: Figure: The plotly figure. diff --git a/obsidian/surrogates/base.py b/obsidian/surrogates/base.py index 796dd31..b827874 100644 --- a/obsidian/surrogates/base.py +++ b/obsidian/surrogates/base.py @@ -104,25 +104,30 @@ def _prepare(self, def fit(self, X: pd.DataFrame, y: pd.Series): + """Fit the surrogate model to data""" pass # pragma: no cover @abstractmethod def predict(self, X: pd.DataFrame): + """Predict outputs based on candidates X""" pass # pragma: no cover @abstractmethod def score(self, X: pd.DataFrame, y: pd.Series): + """Score the model based on the given test data""" pass # pragma: no cover @abstractmethod def save_state(self): + """Save the model to a state dictionary""" pass # pragma: no cover @classmethod @abstractmethod def load_state(cls, obj_dict: dict): + """Load the model from a state dictionary""" pass # pragma: no cover diff --git a/obsidian/surrogates/botorch.py b/obsidian/surrogates/botorch.py index c6e21ce..9ddfaa0 100644 --- a/obsidian/surrogates/botorch.py +++ b/obsidian/surrogates/botorch.py @@ -25,6 +25,22 @@ class SurrogateBoTorch(SurrogateModel): BoTorch GP model, subclass of the obsidian SurrogateModel Attributes: + model_type (str): The type of model to be used. + + Defaults to ``'GP'``. Options are as follows: + + - ``'GP'``: Gaussian Process with default settings (Matern Kernel, Gamma covariance priors) + - ``'MixedGP'``: GP with mixed parameter types (continuous, categorical). Will be re-selected + by default if 'GP' is selected and input space is mixed. + - ``'DKL'``: GP with a NN feature-extractor (deep kernel learning) + - ``'GPflat'``: GP without priors. May result in optimization instability, but removes bias + for special situations. + - ``'GPprior'``: GP with custom priors on the mean, likelihood, and covariance + - ``'MTGP'``: Multi-task GP for multi-output optimization. Will be re-selected by default + if 'GP' is selected and the input space contains Task parameters. + - ``'DNN'``: Dropout neural network. Uses MC sampling to mask neurons during training and + to estimate uncertainty. + device (str): The device on which the model is run. hps (dict): Optional surrogate function hyperparameters. mll (ExactMarginalLogLikelihood): The marginal log likelihood of the model. @@ -110,7 +126,7 @@ def fit(self, Args: X (pd.DataFrame): Input parameters for the training data y (pd.Series): Training data responses - cat_dims (list, optional): A list of indices for categorical dimensions in the input data. Default is None. + cat_dims (list, optional): A list of indices for categorical dimensions in the input data. Default is ``None``. Returns: None. Updates the surrogate model attributes, including regressed parameters. diff --git a/obsidian/surrogates/custom_GP.py b/obsidian/surrogates/custom_GP.py index 7f46fb4..0eddd8f 100644 --- a/obsidian/surrogates/custom_GP.py +++ b/obsidian/surrogates/custom_GP.py @@ -51,6 +51,7 @@ def __init__(self, train_X, train_Y): ) def forward(self, x): + """Evaluate the forward pass of the model on inputs X""" mean_x = self.mean_module(x) covar_x = self.covar_module(x) return MultivariateNormal(mean_x, covar_x) @@ -74,6 +75,7 @@ def __init__(self, train_X, train_Y, nu=2.5): ) def forward(self, x): + """Evaluate the forward pass of the model on inputs X""" mean_x = self.mean_module(x) covar_x = self.covar_module(x) return MultivariateNormal(mean_x, covar_x) @@ -113,7 +115,7 @@ def __init__(self, train_X, train_Y): self.scale_to_bounds = gpytorch.utils.grid.ScaleToBounds(0, 1) def forward(self, x): - + """Evaluate the forward pass of the model on inputs X""" # Pass the data through the feature extractor and scaler projected_x = self.feature_extractor(x) projected_x = self.scale_to_bounds(projected_x) diff --git a/obsidian/surrogates/custom_torch.py b/obsidian/surrogates/custom_torch.py index 0b68356..7a40798 100644 --- a/obsidian/surrogates/custom_torch.py +++ b/obsidian/surrogates/custom_torch.py @@ -45,7 +45,7 @@ def forward(self, x = self.input_layer(x) x = self.middle_layers(x) x = self.outer_layer(x) - + """Evaluate the forward pass of the model on inputs X""" return x def posterior(self, @@ -53,7 +53,7 @@ def posterior(self, n_sample: int = 1024, output_indices: list[int] = None, observation_noise: bool | Tensor = False) -> Posterior: - + """Calculates the posterior distribution of the model at X""" if not output_indices: output_indices = list(range(self._num_outputs)) elif not all(0 <= i < self._num_outputs for i in output_indices): @@ -74,4 +74,5 @@ def posterior(self, @property def num_outputs(self) -> int: + """Number of outputs of the model""" return self._num_outputs diff --git a/obsidian/tests/test_optimizer_SOO.py b/obsidian/tests/test_optimizer_SOO.py index 7849c41..f63a725 100644 --- a/obsidian/tests/test_optimizer_SOO.py +++ b/obsidian/tests/test_optimizer_SOO.py @@ -104,7 +104,7 @@ def test_optimizer_suggest(m_batch, fixed_var): df_suggest = pd.concat([X_suggest, eval_suggest], axis=1) -test_aqs = ['NEI', 'EI', {'EI': {'Xi_f': 0.05}}, {'EI': {'Xi_f': -0.05}}, +test_aqs = ['NEI', 'EI', {'EI': {'inflate': 0.05}}, {'EI': {'inflate': -0.05}}, 'PI', 'UCB', {'UCB': {'beta': 2}}, {'UCB': {'beta': 0}}, 'NIPV', 'SF', 'RS', 'Mean', 'SR'] diff --git a/pyproject.toml b/pyproject.toml index 035d2ea..1aba526 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,40 @@ [tool.poetry] name = "obsidian-apo" -version = "0.7.11" +version = "0.7.12" description = "Automated experiment design and black-box optimization" -authors = ["Kevin Stone ", "Yuting Xu