Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release docs #11

Merged
merged 8 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion obsidian/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 3 additions & 2 deletions obsidian/acquisition/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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': {},
Expand Down
6 changes: 6 additions & 0 deletions obsidian/acquisition/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
12 changes: 6 additions & 6 deletions obsidian/campaign/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
11 changes: 10 additions & 1 deletion obsidian/campaign/campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions obsidian/campaign/explainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -114,13 +116,15 @@ 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.')

fig = shap.summary_plot(self.shap['values'], self.shap['X_sample'])
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.')

Expand All @@ -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.')

Expand All @@ -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.')

Expand All @@ -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
8 changes: 4 additions & 4 deletions obsidian/constraints/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions obsidian/constraints/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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

Expand Down
7 changes: 4 additions & 3 deletions obsidian/experiment/design.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions obsidian/experiment/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions obsidian/experiment/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 4 additions & 2 deletions obsidian/objectives/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,16 +37,17 @@ 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())}

return obj_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})'
2 changes: 2 additions & 0 deletions obsidian/objectives/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Method pointers and config for objective functions"""

from .custom import (
Identity_Objective,
Feature_Objective,
Expand Down
Loading
Loading