diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a1df1a7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: +- repo: /~https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.5.0 + hooks: + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format diff --git a/entmoot/__init__.py b/entmoot/__init__.py index 9ab9456..fab6f5a 100644 --- a/entmoot/__init__.py +++ b/entmoot/__init__.py @@ -1,4 +1,3 @@ -from entmoot.problem_config import ProblemConfig from entmoot.models.enting import Enting from entmoot.models.model_params import ( EntingParams, @@ -8,3 +7,4 @@ ) from entmoot.optimizers.gurobi_opt import GurobiOptimizer from entmoot.optimizers.pyomo_opt import PyomoOptimizer +from entmoot.problem_config import ProblemConfig as ProblemConfig diff --git a/entmoot/benchmarks.py b/entmoot/benchmarks.py index 2fcf3ec..36c6721 100644 --- a/entmoot/benchmarks.py +++ b/entmoot/benchmarks.py @@ -152,10 +152,11 @@ def compute_objectives(xi: Sequence, no_cat=False): f"Allowed values are 1 and 2" ) + def build_reals_only_problem(problem_config: ProblemConfig): - """A problem containing only real values, as used to demonstrate the NChooseK + """A problem containing only real values, as used to demonstrate the NChooseK constraint. - + The minimum is (1.0, 2.0, 3.0, ...)""" problem_config.add_feature("real", (0.0, 5.0), name="x1") @@ -165,10 +166,11 @@ def build_reals_only_problem(problem_config: ProblemConfig): problem_config.add_feature("real", (0.0, 5.0), name="x5") problem_config.add_min_objective() + def eval_reals_only_testfunc(X: ArrayLike): """The function (x1 - 1)**2 + (x2 - 2)**2 + ...""" x = np.atleast_2d(X) xbar = np.ones_like(x) xbar *= (np.arange(x.shape[1]) + 1)[None, :] - y = np.sum((x - xbar)**2, axis=1) - return y.reshape(-1, 1) \ No newline at end of file + y = np.sum((x - xbar) ** 2, axis=1) + return y.reshape(-1, 1) diff --git a/entmoot/constraints.py b/entmoot/constraints.py index b29919b..dcd47bb 100644 --- a/entmoot/constraints.py +++ b/entmoot/constraints.py @@ -65,7 +65,9 @@ def apply_pyomo_constraints( for constraint in self._constraints: features = constraint._get_feature_vars(model, feat_list) if not isinstance(constraint, ExpressionConstraint): - raise TypeError("Only ExpressionConstraints are supported in a constraint list") + raise TypeError( + "Only ExpressionConstraints are supported in a constraint list" + ) expr = constraint._get_expr(model, features) pyo_constraint_list.add(expr) diff --git a/entmoot/models/enting.py b/entmoot/models/enting.py index 808ed6b..0f4f410 100644 --- a/entmoot/models/enting.py +++ b/entmoot/models/enting.py @@ -2,13 +2,13 @@ import numpy as np -from entmoot import ProblemConfig from entmoot.models.base_model import BaseModel from entmoot.models.mean_models.tree_ensemble import TreeEnsemble from entmoot.models.model_params import EntingParams from entmoot.models.uncertainty_models.distance_based_uncertainty import ( DistanceBasedUncertainty, ) +from entmoot.problem_config import ProblemConfig from entmoot.utils import sample @@ -52,7 +52,9 @@ class Enting(BaseModel): X_opt_pyo, _, _ = opt_pyo.solve(enting) """ - def __init__(self, problem_config: ProblemConfig, params: Union[EntingParams, dict, None]): + def __init__( + self, problem_config: ProblemConfig, params: Union[EntingParams, dict, None] + ): if params is None: params = {} if isinstance(params, dict): @@ -128,9 +130,9 @@ def predict(self, X: np.ndarray, is_enc=False) -> list: f"Expected '(num_samples, {len(self._problem_config.feat_list)})', got '{X.shape}'." ) - mean_pred = self.mean_model.predict(X) #.tolist() + mean_pred = self.mean_model.predict(X) # .tolist() unc_pred = self.unc_model.predict(X) - + mean_pred = self._problem_config.transform_objective(mean_pred) mean_pred = mean_pred.tolist() @@ -147,7 +149,9 @@ def predict_acq(self, X: np.ndarray, is_enc=False) -> list: acq_pred.append(mean + self._beta * unc) return acq_pred - def add_to_gurobipy_model(self, core_model, weights: Optional[tuple[float, ...]] = None) -> None: + def add_to_gurobipy_model( + self, core_model, weights: Optional[tuple[float, ...]] = None + ) -> None: """ Enriches the core model by adding variables and constraints based on information from the tree model. @@ -173,7 +177,9 @@ def add_to_gurobipy_model(self, core_model, weights: Optional[tuple[float, ...]] if weights is not None: moo_weights = weights else: - moo_weights = sample(len(self._problem_config.obj_list), 1, self._problem_config.rng)[0] + moo_weights = sample( + len(self._problem_config.obj_list), 1, self._problem_config.rng + )[0] for idx, obj in enumerate(self._problem_config.obj_list): core_model.addConstr( @@ -184,7 +190,9 @@ def add_to_gurobipy_model(self, core_model, weights: Optional[tuple[float, ...]] core_model.setObjective(core_model._mu + self._beta * core_model._unc) core_model.update() - def add_to_pyomo_model(self, core_model, weights: Optional[tuple[float, ...]] = None) -> None: + def add_to_pyomo_model( + self, core_model, weights: Optional[tuple[float, ...]] = None + ) -> None: """ Enriches the core model by adding variables and constraints based on information from the tree model. @@ -212,7 +220,9 @@ def add_to_pyomo_model(self, core_model, weights: Optional[tuple[float, ...]] = if weights is not None: moo_weights = weights else: - moo_weights = sample(len(self._problem_config.obj_list), 1, self._problem_config.rng)[0] + moo_weights = sample( + len(self._problem_config.obj_list), 1, self._problem_config.rng + )[0] objectives_position_name = list( enumerate([obj.name for obj in self._problem_config.obj_list]) diff --git a/entmoot/models/mean_models/lgbm_utils.py b/entmoot/models/mean_models/lgbm_utils.py index ffde0cc..fca9632 100644 --- a/entmoot/models/mean_models/lgbm_utils.py +++ b/entmoot/models/mean_models/lgbm_utils.py @@ -2,7 +2,6 @@ def read_lgbm_tree_model_dict(tree_model_dict, cat_idx): ordered_tree_list = [] for tree in tree_model_dict["tree_info"]: - # generate list of nodes in tree root_node = [tree["tree_structure"]] node_list = [] diff --git a/entmoot/models/mean_models/tree_ensemble.py b/entmoot/models/mean_models/tree_ensemble.py index a9a098b..5973d0d 100644 --- a/entmoot/models/mean_models/tree_ensemble.py +++ b/entmoot/models/mean_models/tree_ensemble.py @@ -8,11 +8,15 @@ from entmoot.models.mean_models.lgbm_utils import read_lgbm_tree_model_dict from entmoot.models.mean_models.meta_tree_ensemble import MetaTreeModel from entmoot.models.model_params import TreeTrainParams -from entmoot.problem_config import ProblemConfig, Categorical +from entmoot.problem_config import Categorical, ProblemConfig class TreeEnsemble(BaseModel): - def __init__(self, problem_config: ProblemConfig, params: Union[TreeTrainParams, dict, None] = None): + def __init__( + self, + problem_config: ProblemConfig, + params: Union[TreeTrainParams, dict, None] = None, + ): if params is None: params = {} if isinstance(params, dict): @@ -98,14 +102,15 @@ def _train_lgbm(self, X, y): tree_model = lgb.train( self._train_params, train_data, - #verbose_eval=False, + # verbose_eval=False, ) else: # train for non-categorical vars train_data = lgb.Dataset(X, label=y, params={"verbose": -1}) tree_model = lgb.train( - self._train_params, train_data#, verbose_eval=False + self._train_params, + train_data, # , verbose_eval=False ) return tree_model diff --git a/entmoot/models/model_params.py b/entmoot/models/model_params.py index 51677da..57e939e 100644 --- a/entmoot/models/model_params.py +++ b/entmoot/models/model_params.py @@ -6,13 +6,16 @@ class ParamValidationError(ValueError): """A model parameter takes an invalid value.""" + pass + @dataclass class UncParams: """ This dataclass contains all uncertainty parameters. """ + #: weight for penalty/exploration part in objective function beta: float = 1.96 #: the predictions of the GBT model are cut off, if their absolute value exceeds @@ -35,12 +38,12 @@ def __post_init__(self): raise ParamValidationError( f"Value for 'beta' is {self.beta}; must be positive." ) - + if self.acq_sense not in ("exploration", "penalty"): raise ParamValidationError( f"Value for 'acq_sense' is '{self.acq_sense}'; must be in ('exploration', 'penalty')." ) - + @dataclass class TrainParams: @@ -48,6 +51,7 @@ class TrainParams: This dataclass contains all hyperparameters that are used by lightbm during training and documented here https://lightgbm.readthedocs.io/en/latest/Parameters.html """ + # lightgbm training hyperparameters objective: str = "regression" metric: str = "rmse" @@ -64,13 +68,14 @@ class TreeTrainParams: """ This dataclass contains all parameters needed for the tree training. """ - train_params: "TrainParams" = field(default_factory=dict) # type: ignore + + train_params: "TrainParams" = field(default_factory=dict) # type: ignore train_lib: Literal["lgbm"] = "lgbm" def __post_init__(self): if isinstance(self.train_params, dict): self.train_params = TrainParams(**self.train_params) - + if self.train_lib not in ("lgbm",): raise ParamValidationError( f"Value for 'train_lib' is {self.train_lib}; must be in ('lgbm',)" @@ -80,15 +85,16 @@ def __post_init__(self): @dataclass class EntingParams: """Contains parameters for a mean and uncertainty model. - - Provides a structured dataclass for the parameters of an Enting model, + + Provides a structured dataclass for the parameters of an Enting model, alongside default values and some light data validation.""" - unc_params: "UncParams" = field(default_factory=dict) # type: ignore - tree_train_params: "TreeTrainParams" = field(default_factory=dict) # type: ignore - + + unc_params: "UncParams" = field(default_factory=dict) # type: ignore + tree_train_params: "TreeTrainParams" = field(default_factory=dict) # type: ignore + def __post_init__(self): if isinstance(self.unc_params, dict): self.unc_params = UncParams(**self.unc_params) if isinstance(self.tree_train_params, dict): - self.tree_train_params = TreeTrainParams(**self.tree_train_params) \ No newline at end of file + self.tree_train_params = TreeTrainParams(**self.tree_train_params) diff --git a/entmoot/models/uncertainty_models/base_distance.py b/entmoot/models/uncertainty_models/base_distance.py index 0ad7f39..8e532b6 100644 --- a/entmoot/models/uncertainty_models/base_distance.py +++ b/entmoot/models/uncertainty_models/base_distance.py @@ -3,7 +3,7 @@ import numpy as np from entmoot.models.base_model import BaseModel -from entmoot.problem_config import ProblemConfig, Categorical +from entmoot.problem_config import Categorical, ProblemConfig class NonCatDistance(BaseModel): @@ -74,7 +74,7 @@ def get_gurobipy_model_constr(self, model_core): def get_pyomo_model_constr(self, model_core): raise NotImplementedError() - + def get_gurobipy_model_constr_terms(self, model) -> list: raise NotImplementedError() @@ -82,7 +82,6 @@ def get_pyomo_model_constr_terms(self, model) -> list: raise NotImplementedError() - class CatDistance(BaseModel): def __init__(self, problem_config: ProblemConfig, acq_sense): self._problem_config = problem_config diff --git a/entmoot/models/uncertainty_models/distance_based_uncertainty.py b/entmoot/models/uncertainty_models/distance_based_uncertainty.py index 6ef3f81..fdf1320 100644 --- a/entmoot/models/uncertainty_models/distance_based_uncertainty.py +++ b/entmoot/models/uncertainty_models/distance_based_uncertainty.py @@ -17,12 +17,20 @@ @overload -def distance_func_mapper(dist_name: str, cat: Literal[True]) -> type[CatDistance] | None: ... +def distance_func_mapper( + dist_name: str, cat: Literal[True] +) -> type[CatDistance] | None: ... + @overload -def distance_func_mapper(dist_name: str, cat: Literal[False]) -> type[NonCatDistance] | None: ... +def distance_func_mapper( + dist_name: str, cat: Literal[False] +) -> type[NonCatDistance] | None: ... + -def distance_func_mapper(dist_name: str, cat: bool) -> type[CatDistance] | type[NonCatDistance] | None: +def distance_func_mapper( + dist_name: str, cat: bool +) -> type[CatDistance] | type[NonCatDistance] | None: """Given a string, return the distance function""" non_cat_dists = { "euclidean_squared": EuclideanSquaredDistance, @@ -41,7 +49,9 @@ def distance_func_mapper(dist_name: str, cat: bool) -> type[CatDistance] | type[ class DistanceBasedUncertainty(BaseModel): - def __init__(self, problem_config: ProblemConfig, params: Union[UncParams, dict, None] = None): + def __init__( + self, problem_config: ProblemConfig, params: Union[UncParams, dict, None] = None + ): if params is None: params = {} if isinstance(params, dict): @@ -100,7 +110,7 @@ def __init__(self, problem_config: ProblemConfig, params: Union[UncParams, dict, ) else: self.cat_unc_model: CatDistance = cat_distance( - problem_config=self._problem_config, + problem_config=self._problem_config, acq_sense=params.acq_sense, ) @@ -171,7 +181,7 @@ def add_to_gurobipy_model(self, model): model.addVar(name=f"bin_penalty_{i}", vtype="B") ) - big_m_term = big_m * (1 - model._bin_penalty[-1]) # type: ignore + big_m_term = big_m * (1 - model._bin_penalty[-1]) # type: ignore if self._dist_metric == "l2": # take sqrt for l2 distance @@ -265,7 +275,11 @@ def add_to_pyomo_model(self, model): def constrs_bin_penalty_sum(model_obj): return ( - sum(model_obj._bin_penalty[k] for k in model.indices_constrs_cat_noncat_contr) == 1 + sum( + model_obj._bin_penalty[k] + for k in model.indices_constrs_cat_noncat_contr + ) + == 1 ) model.constrs_bin_penalty_sum = pyo.Constraint(rule=constrs_bin_penalty_sum) diff --git a/entmoot/models/uncertainty_models/euclidean_squared_distance.py b/entmoot/models/uncertainty_models/euclidean_squared_distance.py index 85bbe41..ab325cf 100644 --- a/entmoot/models/uncertainty_models/euclidean_squared_distance.py +++ b/entmoot/models/uncertainty_models/euclidean_squared_distance.py @@ -15,7 +15,6 @@ def _array_predict(self, X): raise NotImplementedError() def get_gurobipy_model_constr_terms(self, model): - from gurobipy import quicksum features = model._all_feat @@ -31,7 +30,6 @@ def get_gurobipy_model_constr_terms(self, model): return constr_list def get_pyomo_model_constr_terms(self, model): - features = model._all_feat constr_list = [] diff --git a/entmoot/models/uncertainty_models/l2_distance.py b/entmoot/models/uncertainty_models/l2_distance.py index 97f9384..6ff5a3a 100644 --- a/entmoot/models/uncertainty_models/l2_distance.py +++ b/entmoot/models/uncertainty_models/l2_distance.py @@ -30,7 +30,6 @@ def get_gurobipy_model_constr_terms(self, model): return constr_list def get_pyomo_model_constr_terms(self, model): - features = model._all_feat constr_list = [] diff --git a/entmoot/optimizers/gurobi_opt.py b/entmoot/optimizers/gurobi_opt.py index 9a32909..5673898 100644 --- a/entmoot/optimizers/gurobi_opt.py +++ b/entmoot/optimizers/gurobi_opt.py @@ -4,12 +4,13 @@ import gurobipy as gur import numpy as np -from entmoot import Enting, ProblemConfig +from entmoot.models.enting import Enting +from entmoot.problem_config import Categorical, ProblemConfig from entmoot.utils import OptResult -from entmoot.problem_config import Categorical ActiveLeavesT = list[list[tuple[int, str]]] + class GurobiOptimizer: """ This class builds and solves a Gurobi optimization model using available @@ -47,6 +48,7 @@ class GurobiOptimizer: # As expected, the optimal input of the tree model is near the origin (cf. X_opt_pyo) X_opt_pyo, _, _ = opt_gur.solve(enting) """ + def __init__(self, problem_config: ProblemConfig, params: Optional[dict] = None): self._params = {} if params is None else params self._problem_config = problem_config @@ -128,7 +130,9 @@ def solve( self._active_leaves, ) - def _get_sol(self, solved_model: gur.Model) -> tuple[list | np.ndarray, ActiveLeavesT]: + def _get_sol( + self, solved_model: gur.Model + ) -> tuple[list | np.ndarray, ActiveLeavesT]: # extract solutions from conti and discrete variables res = [] for idx, feat in enumerate(self._problem_config.feat_list): diff --git a/entmoot/optimizers/pyomo_opt.py b/entmoot/optimizers/pyomo_opt.py index 311d0f1..a878e03 100644 --- a/entmoot/optimizers/pyomo_opt.py +++ b/entmoot/optimizers/pyomo_opt.py @@ -3,12 +3,13 @@ import numpy as np import pyomo.environ as pyo -from entmoot import Enting, ProblemConfig +from entmoot.models.enting import Enting +from entmoot.problem_config import Categorical, ProblemConfig from entmoot.utils import OptResult -from entmoot.problem_config import Categorical ActiveLeavesT = list[list[tuple[int, str]]] + class PyomoOptimizer: """ This class builds and solves a Pyomo optimization model using available @@ -48,6 +49,7 @@ class PyomoOptimizer: # As expected, the optimal input of the tree model is near the origin (cf. X_opt_pyo) X_opt_pyo, _, _ = opt_pyo.solve(enting) """ + def __init__(self, problem_config: ProblemConfig, params: Optional[dict] = None): self._params = {} if params is None else params self._problem_config = problem_config @@ -70,7 +72,10 @@ def get_active_leaf_sol(self) -> ActiveLeavesT: return self._active_leaves def solve( - self, tree_model: Enting, model_core: Optional[pyo.ConcreteModel] = None, weights: Optional[tuple[float, ...]] = None + self, + tree_model: Enting, + model_core: Optional[pyo.ConcreteModel] = None, + weights: Optional[tuple[float, ...]] = None, ) -> OptResult: """ Solves the Pyomo optimization model @@ -116,10 +121,12 @@ def solve( pyo.value(opt_model.obj), [opt_model._unscaled_mu[k].value for k in opt_model._unscaled_mu], pyo.value(opt_model._unc), - self._active_leaves + self._active_leaves, ) - def _get_sol(self, solved_model: pyo.ConcreteModel) -> tuple[list | np.ndarray, ActiveLeavesT]: + def _get_sol( + self, solved_model: pyo.ConcreteModel + ) -> tuple[list | np.ndarray, ActiveLeavesT]: # extract solutions from conti and discrete variables res = [] for idx, feat in enumerate(self._problem_config.feat_list): @@ -144,7 +151,12 @@ def obj_leaf_index(model_obj, obj_name): act_leaves = [] for idx, obj in enumerate(self._problem_config.obj_list): act_leaves.append( - [(tree_id, leaf_enc) for tree_id, leaf_enc in obj_leaf_index(solved_model, obj.name) - if round(pyo.value(solved_model._z[obj.name, tree_id, leaf_enc])) == 1.0]) + [ + (tree_id, leaf_enc) + for tree_id, leaf_enc in obj_leaf_index(solved_model, obj.name) + if round(pyo.value(solved_model._z[obj.name, tree_id, leaf_enc])) + == 1.0 + ] + ) return self._problem_config.decode([res]), act_leaves diff --git a/entmoot/problem_config.py b/entmoot/problem_config.py index 4086775..1591781 100644 --- a/entmoot/problem_config.py +++ b/entmoot/problem_config.py @@ -1,11 +1,12 @@ -from typing import List, Optional, TypeVar from abc import ABC, abstractmethod +from typing import List, Optional, TypeVar import numpy as np BoundsT = tuple[float, float] CategoriesT = list[str | float | int] | tuple[str | float | int, ...] + class FeatureType(ABC): def __init__(self, name: str): self.name = name @@ -111,20 +112,25 @@ def decode(self, xi): def is_bin(self): return True + class Objective: def __init__(self, name): self.name = name + class MinObjective(Objective): sign = 1 + class MaxObjective(Objective): sign = -1 + AnyFeatureT = Real | Integer | Categorical | Binary FeatureT = TypeVar("FeatureT", bound=FeatureType) AnyObjectiveT = MinObjective | MaxObjective + class ProblemConfig: def __init__(self, rnd_seed: Optional[int] = None): self._feat_list = [] @@ -134,22 +140,42 @@ def __init__(self, rnd_seed: Optional[int] = None): @property def cat_idx(self): - return tuple([i for i, feat in enumerate(self.feat_list) if isinstance(feat, Categorical)]) + return tuple( + [ + i + for i, feat in enumerate(self.feat_list) + if isinstance(feat, Categorical) + ] + ) @property def non_cat_idx(self): - return tuple([i for i, feat in enumerate(self.feat_list) if not isinstance(feat, Categorical)]) + return tuple( + [ + i + for i, feat in enumerate(self.feat_list) + if not isinstance(feat, Categorical) + ] + ) @property def non_cat_lb(self): return tuple( - [feat.lb for i, feat in enumerate(self.feat_list) if not isinstance(feat, Categorical)] + [ + feat.lb + for i, feat in enumerate(self.feat_list) + if not isinstance(feat, Categorical) + ] ) @property def non_cat_ub(self): return tuple( - [feat.ub for i, feat in enumerate(self.feat_list) if not isinstance(feat, Categorical)] + [ + feat.ub + for i, feat in enumerate(self.feat_list) + if not isinstance(feat, Categorical) + ] ) @property @@ -162,8 +188,16 @@ def non_cat_bnd_diff(self): ] ) - def get_idx_and_feat_by_type(self, feature_type: type[FeatureT]) -> tuple[tuple[int, FeatureT], ...]: - return tuple([(i, feat) for i, feat in enumerate(self.feat_list) if isinstance(feat, feature_type)]) + def get_idx_and_feat_by_type( + self, feature_type: type[FeatureT] + ) -> tuple[tuple[int, FeatureT], ...]: + return tuple( + [ + (i, feat) + for i, feat in enumerate(self.feat_list) + if isinstance(feat, feature_type) + ] + ) @property def feat_list(self) -> list[AnyFeatureT]: @@ -206,7 +240,12 @@ def decode(self, X: List): dec = [self._decode_xi(xi) for xi in X] return np.asarray(dec) - def add_feature(self, feat_type: str, bounds: Optional[BoundsT | CategoriesT] = None, name: Optional[str] = None): + def add_feature( + self, + feat_type: str, + bounds: Optional[BoundsT | CategoriesT] = None, + name: Optional[str] = None, + ): if name is None: name = f"feat_{len(self.feat_list)}" @@ -255,7 +294,7 @@ def add_feature(self, feat_type: str, bounds: Optional[BoundsT | CategoriesT] = f"smaller than upper bound. Check feature '{name}'." ) - self._feat_list.append(Integer(lb=lb, ub=ub, name=name)) + self._feat_list.append(Integer(lb=lb, ub=ub, name=name)) elif feat_type == "categorical": assert len(bounds) > 1, ( @@ -273,7 +312,7 @@ def add_feature(self, feat_type: str, bounds: Optional[BoundsT | CategoriesT] = set(bounds) ), f"Categories of feat_type '{feat_type}' are not all unique." - self._feat_list.append(Categorical(cat_list=bounds, name=name)) # type: ignore + self._feat_list.append(Categorical(cat_list=bounds, name=name)) # type: ignore else: raise ValueError( @@ -298,8 +337,6 @@ def transform_objective(self, y: np.ndarray) -> np.ndarray: signs = np.array([obj.sign for obj in self.obj_list]).reshape(1, -1) return y * signs - - def get_rnd_sample_numpy(self, num_samples): # returns np.array for faster processing # TODO: defer sample logic to feature @@ -315,9 +352,7 @@ def get_rnd_sample_numpy(self, num_samples): ) else: array_list.append( - self.rng.integers( - low=feat.lb, high=feat.ub+1, size=num_samples - ) + self.rng.integers(low=feat.lb, high=feat.ub + 1, size=num_samples) ) return np.squeeze(np.column_stack(array_list)) @@ -335,7 +370,7 @@ def get_rnd_sample_list(self, num_samples=1, cat_enc=False): else: sample.append(self.rng.choice(feat.cat_list)) else: - sample.append(self.rng.integers(feat.lb, feat.ub+1)) + sample.append(self.rng.integers(feat.lb, feat.ub + 1)) sample_list.append(tuple(sample)) return sample_list if len(sample_list) > 1 else sample_list[0] @@ -495,5 +530,3 @@ def __str__(self): for obj in self.obj_list: out_str.append(f"{obj.name} :: {obj.__class__.__name__}") return "\n".join(out_str) - - diff --git a/entmoot/utils.py b/entmoot/utils.py index 8f51520..a8c8312 100644 --- a/entmoot/utils.py +++ b/entmoot/utils.py @@ -64,7 +64,9 @@ def grid(dimension: int, levels: int) -> np.ndarray: return out / n -def sample(dimension: int, n_samples: int = 1, rng: Optional[np.random.Generator] = None) -> np.ndarray: +def sample( + dimension: int, n_samples: int = 1, rng: Optional[np.random.Generator] = None +) -> np.ndarray: """Sample uniformly from the unit simplex. Args: @@ -74,7 +76,7 @@ def sample(dimension: int, n_samples: int = 1, rng: Optional[np.random.Generator Returns: array, shape=(n_samples, dimesnion): Random samples from the unit simplex. """ - if rng is None: + if rng is None: rng = np.random.default_rng() s = rng.standard_exponential((n_samples, dimension)) return (s.T / s.sum(axis=1)).T diff --git a/pyproject.toml b/pyproject.toml index f995ca4..2a71d0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,6 @@ markers = [ [tool.ruff.lint] select = ["E4", "E7", "E9", "F", "I001"] ignore = ["E721", "E731", "F722", "F821"] -ignore-init-module-imports = true [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] diff --git a/requirements-dev.txt b/requirements-dev.txt index a95d6f2..7d02f20 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,3 +6,4 @@ pytest-cov IPython nbsphinx sphinx-rtd-theme +pre-commit \ No newline at end of file diff --git a/setup.py b/setup.py index 16096ec..ebe0ffb 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ -from setuptools import setup, find_packages from pathlib import Path +from setuptools import find_packages, setup + project_root = Path(__file__).resolve().parent about = {} diff --git a/tests/test_bring_your_own_constraints.py b/tests/test_bring_your_own_constraints.py index db735e5..9e3b6dc 100644 --- a/tests/test_bring_your_own_constraints.py +++ b/tests/test_bring_your_own_constraints.py @@ -1,4 +1,4 @@ -from entmoot import Enting, ProblemConfig, GurobiOptimizer, PyomoOptimizer +from entmoot import Enting, GurobiOptimizer, ProblemConfig from entmoot.benchmarks import ( build_multi_obj_categorical_problem, eval_multi_obj_cat_testfunc, @@ -42,4 +42,4 @@ def test_bring_your_own_constraints(): res_gur = opt_gur.solve(enting, model_core=model_gur) x_opt, y_opt, z_opt = res_gur.opt_point[3:] - assert round(x_opt, 5) == round(y_opt, 5) and round(y_opt, 5) == round(z_opt, 5) \ No newline at end of file + assert round(x_opt, 5) == round(y_opt, 5) and round(y_opt, 5) == round(z_opt, 5) diff --git a/tests/test_consistency_gurobi.py b/tests/test_consistency_gurobi.py index f23b949..6030721 100644 --- a/tests/test_consistency_gurobi.py +++ b/tests/test_consistency_gurobi.py @@ -1,14 +1,19 @@ -import pytest - -import random import math +import random + +import pytest -from entmoot import Enting, ProblemConfig, GurobiOptimizer -from entmoot.models.model_params import EntingParams, UncParams, TreeTrainParams, TrainParams +from entmoot import Enting, GurobiOptimizer, ProblemConfig from entmoot.benchmarks import ( build_multi_obj_categorical_problem, eval_multi_obj_cat_testfunc, ) +from entmoot.models.model_params import ( + EntingParams, + TrainParams, + TreeTrainParams, + UncParams, +) def get_leaf_center(bnds): @@ -115,9 +120,8 @@ def test_gurobi_consistency3(rnd_seed, n_obj, acq_sense, dist_metric, cat_metric dist_metric=dist_metric, acq_sense=acq_sense, dist_trafo="normal", - cat_metric=cat_metric + cat_metric=cat_metric, ), - # make tree model smaller to reduce testing time tree_train_params=TreeTrainParams( train_lib="lgbm", @@ -129,9 +133,9 @@ def test_gurobi_consistency3(rnd_seed, n_obj, acq_sense, dist_metric, cat_metric max_depth=2, min_data_in_leaf=1, min_data_per_group=1, - verbose=-1 - ) - ) + verbose=-1, + ), + ), ) params.unc_params.beta = 0.05 diff --git a/tests/test_consistency_pyomo.py b/tests/test_consistency_pyomo.py index a69c11a..322a881 100644 --- a/tests/test_consistency_pyomo.py +++ b/tests/test_consistency_pyomo.py @@ -1,14 +1,19 @@ -import pytest - -import random import math +import random + +import pytest from entmoot import Enting, ProblemConfig, PyomoOptimizer -from entmoot.models.model_params import EntingParams, UncParams, TreeTrainParams, TrainParams from entmoot.benchmarks import ( build_multi_obj_categorical_problem, eval_multi_obj_cat_testfunc, ) +from entmoot.models.model_params import ( + EntingParams, + TrainParams, + TreeTrainParams, + UncParams, +) def get_leaf_center(bnds): @@ -123,9 +128,8 @@ def test_pyomo_consistency3(rnd_seed, n_obj, acq_sense, dist_metric, cat_metric) dist_metric=dist_metric, acq_sense=acq_sense, dist_trafo="normal", - cat_metric=cat_metric + cat_metric=cat_metric, ), - # make tree model smaller to reduce testing time tree_train_params=TreeTrainParams( train_lib="lgbm", @@ -137,9 +141,9 @@ def test_pyomo_consistency3(rnd_seed, n_obj, acq_sense, dist_metric, cat_metric) max_depth=2, min_data_in_leaf=1, min_data_per_group=1, - verbose=-1 - ) - ) + verbose=-1, + ), + ), ) params.unc_params.beta = 0.05 diff --git a/tests/test_constraints_pyomo.py b/tests/test_constraints_pyomo.py index 8a5b767..9142e57 100644 --- a/tests/test_constraints_pyomo.py +++ b/tests/test_constraints_pyomo.py @@ -1,26 +1,25 @@ -from entmoot.problem_config import ProblemConfig -from entmoot.models.enting import Enting -from entmoot.optimizers.pyomo_opt import PyomoOptimizer -from entmoot.models.model_params import EntingParams, UncParams -from entmoot.constraints import LinearInequalityConstraint, ConstraintList import pyomo.environ as pyo - +import pytest from entmoot.benchmarks import ( - build_reals_only_problem, - eval_reals_only_testfunc, build_multi_obj_categorical_problem, + build_reals_only_problem, eval_multi_obj_cat_testfunc, + eval_reals_only_testfunc, ) from entmoot.constraints import ( + ConstraintList, LinearEqualityConstraint, + LinearInequalityConstraint, NChooseKConstraint, ) -import pytest +from entmoot.models.enting import Enting +from entmoot.models.model_params import EntingParams, UncParams +from entmoot.optimizers.pyomo_opt import PyomoOptimizer +from entmoot.problem_config import ProblemConfig + +PARAMS = EntingParams(unc_params=UncParams(dist_metric="l1", acq_sense="exploration")) -PARAMS = EntingParams( - unc_params=UncParams(dist_metric="l1", acq_sense="exploration") -) def test_linear_equality_constraint(): problem_config = ProblemConfig(rnd_seed=73) @@ -110,16 +109,14 @@ def test_constraint_list(): # define the constraints constraints = [ NChooseKConstraint( - feature_keys=["x1", "x2", "x3", "x4", "x5"], + feature_keys=["x1", "x2", "x3", "x4", "x5"], min_count=1, max_count=3, - none_also_valid=True + none_also_valid=True, ), LinearInequalityConstraint( - feature_keys=["x3", "x4", "x5"], - coefficients=[1, 1, 1], - rhs=10.0 - ) + feature_keys=["x3", "x4", "x5"], coefficients=[1, 1, 1], rhs=10.0 + ), ] # apply constraints to the model @@ -135,4 +132,4 @@ def test_constraint_list(): print(res_pyo.opt_point) assert 1 <= sum(x > 1e-6 for x in res_pyo.opt_point) <= 3 - assert sum(res_pyo.opt_point[2:]) < 10.0 \ No newline at end of file + assert sum(res_pyo.opt_point[2:]) < 10.0 diff --git a/tests/test_curr.py b/tests/test_curr.py index 3a4a359..d1afcfd 100644 --- a/tests/test_curr.py +++ b/tests/test_curr.py @@ -1,16 +1,17 @@ import math +import random -from entmoot import Enting, ProblemConfig, GurobiOptimizer, PyomoOptimizer -from entmoot.models.model_params import EntingParams, UncParams +import numpy as np +import pyomo.environ # noqa: F401 +import pytest + +from entmoot import Enting, GurobiOptimizer, ProblemConfig, PyomoOptimizer from entmoot.benchmarks import ( build_multi_obj_categorical_problem, eval_multi_obj_cat_testfunc, ) +from entmoot.models.model_params import EntingParams, UncParams -import numpy as np -import pytest -import random -import pyomo.environ # noqa: F401 @pytest.mark.pipeline_test def test_core_model_copy(): @@ -45,10 +46,9 @@ def test_multiobj_constraints(): rnd_sample = problem_config.get_rnd_sample_list(num_samples=20) testfunc_evals = eval_multi_obj_cat_testfunc(rnd_sample, n_obj=number_objectives) - params = EntingParams(unc_params=UncParams( - dist_metric="l1", - acq_sense="exploration" - )) + params = EntingParams( + unc_params=UncParams(dist_metric="l1", acq_sense="exploration") + ) enting = Enting(problem_config, params=params) # fit tree ensemble enting.fit(rnd_sample, testfunc_evals) @@ -114,10 +114,9 @@ def my_func(x: float) -> float: y_train = np.reshape([my_func(x) for x in X_train], (-1, 1)) # Define enting object and corresponding parameters - params = EntingParams(unc_params=UncParams( - dist_metric="l1", - acq_sense="exploration" - )) + params = EntingParams( + unc_params=UncParams(dist_metric="l1", acq_sense="exploration") + ) enting = Enting(problem_config, params=params) # Fit tree model enting.fit(X_train, y_train) @@ -148,10 +147,9 @@ def test_compare_pyomo_gurobipy_multiobj(): for metric in ["l1", "l2", "euclidean_squared"]: for acq_sense in ["exploration", "penalty"]: - params = EntingParams(unc_params=UncParams( - dist_metric=metric, - acq_sense=acq_sense - )) + params = EntingParams( + unc_params=UncParams(dist_metric=metric, acq_sense=acq_sense) + ) enting = Enting(problem_config, params=params) # fit tree ensemble enting.fit(rnd_sample, testfunc_evals) @@ -192,10 +190,9 @@ def test_compare_pyomo_gurobipy_singleobj(): for metric in ["l1", "l2", "euclidean_squared"]: for acq_sense in ["exploration", "penalty"]: - params = EntingParams(unc_params=UncParams( - dist_metric=metric, - acq_sense=acq_sense - )) + params = EntingParams( + unc_params=UncParams(dist_metric=metric, acq_sense=acq_sense) + ) enting = Enting(problem_config, params=params) # fit tree ensemble enting.fit(rnd_sample, testfunc_evals) diff --git a/tests/test_model_params.py b/tests/test_model_params.py index e30ab5f..ff773be 100644 --- a/tests/test_model_params.py +++ b/tests/test_model_params.py @@ -1,14 +1,22 @@ -from entmoot.models.model_params import EntingParams, UncParams, TrainParams, TreeTrainParams, ParamValidationError import pytest +from entmoot.models.model_params import ( + EntingParams, + ParamValidationError, + TrainParams, + TreeTrainParams, + UncParams, +) + + def test_model_params_creation(): """Check EntingParams is instantiated correctly, and check default values.""" - params = EntingParams(**{ - "unc_params": {"beta": 2}, - "tree_train_params" : { - "train_params": {"max_depth": 5} - } - }) + params = EntingParams( + **{ + "unc_params": {"beta": 2}, + "tree_train_params": {"train_params": {"max_depth": 5}}, + } + ) assert params.unc_params.beta == 2 assert params.tree_train_params.train_params.max_depth == 5 @@ -20,9 +28,7 @@ def test_model_params_creation(): # check alternate initialisation method params_other = EntingParams( unc_params=UncParams(beta=2), - tree_train_params=TreeTrainParams( - train_params=TrainParams(max_depth=5) - ) + tree_train_params=TreeTrainParams(train_params=TrainParams(max_depth=5)), ) assert params == params_other @@ -36,4 +42,4 @@ def test_model_params_invalid_values(): _ = EntingParams(**{"tree_train_params": {"train_lib": "notimplementedlib"}}) with pytest.raises(ParamValidationError): - _ = EntingParams(**{"unc_params": {"acq_sense": "notimplementedsense"}}) \ No newline at end of file + _ = EntingParams(**{"unc_params": {"acq_sense": "notimplementedsense"}}) diff --git a/tests/test_objectives_pyomo.py b/tests/test_objectives_pyomo.py index 70767e1..d250a9a 100644 --- a/tests/test_objectives_pyomo.py +++ b/tests/test_objectives_pyomo.py @@ -1,9 +1,11 @@ +from pytest import approx + from entmoot import Enting, ProblemConfig, PyomoOptimizer from entmoot.benchmarks import ( build_multi_obj_categorical_problem, eval_multi_obj_cat_testfunc, ) -from pytest import approx + def test_max_predictions_equal_min_predictions(): """The sign of the predicted objective is independent of max/min.""" @@ -29,11 +31,12 @@ def test_max_predictions_equal_min_predictions(): pred = enting.predict(sample) pred_max = enting_max.predict(sample) - for ((m1, u1), (m2, u2)) in zip(pred, pred_max): + for (m1, u1), (m2, u2) in zip(pred, pred_max): print(">", m1, m2) assert m1 == approx(m2, rel=1e-5) assert u1 == approx(u2, rel=1e-5) + def test_max_objective_equals_minus_min_objective(): """Assert that the solution found by the minimiser is the same as that of the maximiser for the negative objective function""" problem_config = ProblemConfig(rnd_seed=73) @@ -60,6 +63,3 @@ def test_max_objective_equals_minus_min_objective(): res_max = PyomoOptimizer(problem_config_max, params=params_pyomo).solve(enting) assert res.opt_point == approx(res_max.opt_point, rel=1e-5) - - - diff --git a/tests/test_optimality_gurobi.py b/tests/test_optimality_gurobi.py index 234ef66..d57f499 100644 --- a/tests/test_optimality_gurobi.py +++ b/tests/test_optimality_gurobi.py @@ -1,16 +1,16 @@ -import pytest - import math + import numpy as np +import pytest -from entmoot import Enting, ProblemConfig, GurobiOptimizer -from entmoot.models.model_params import EntingParams, UncParams +from entmoot import Enting, GurobiOptimizer, ProblemConfig from entmoot.benchmarks import ( build_multi_obj_categorical_problem, - eval_multi_obj_cat_testfunc, build_small_single_obj_categorical_problem, + eval_multi_obj_cat_testfunc, eval_small_single_obj_cat_testfunc, ) +from entmoot.models.model_params import EntingParams, UncParams def run_gurobi( @@ -66,7 +66,7 @@ def run_gurobi( est_pred_sol, obj, abs_tol=1e-1 ), f"`{est_pred_sol}` and `{obj}` optimal values are not the same" - assert est_pred_sol >= obj, f"estimated value is smaller than solver solution" + assert est_pred_sol >= obj, "estimated value is smaller than solver solution" # check that mu values are sufficiently close est_mu = enting.mean_model.predict([samples[min_idx]])[0] diff --git a/tests/test_optimality_pyomo.py b/tests/test_optimality_pyomo.py index d08c8be..f29b9dd 100644 --- a/tests/test_optimality_pyomo.py +++ b/tests/test_optimality_pyomo.py @@ -1,16 +1,16 @@ -import pytest - import math + import numpy as np +import pytest from entmoot import Enting, ProblemConfig, PyomoOptimizer -from entmoot.models.model_params import EntingParams, UncParams from entmoot.benchmarks import ( build_multi_obj_categorical_problem, - eval_multi_obj_cat_testfunc, build_small_single_obj_categorical_problem, + eval_multi_obj_cat_testfunc, eval_small_single_obj_cat_testfunc, ) +from entmoot.models.model_params import EntingParams, UncParams def run_pyomo( @@ -66,7 +66,7 @@ def run_pyomo( est_pred_sol, obj, abs_tol=1e-1 ), f"`{est_pred_sol}` and `{obj}` optimal values are not the same" - assert est_pred_sol >= obj, f"estimated value is smaller than solver solution" + assert est_pred_sol >= obj, "estimated value is smaller than solver solution" # check that mu values are sufficiently close est_mu = enting.mean_model.predict([samples[min_idx]])[0]