Source code for smt.design_space.design_space

"""
Author: Jasper Bussemaker <jasper.bussemaker@dlr.de>

This package is distributed under New BSD license.
"""

from typing import List, Optional, Sequence, Tuple, Union

import numpy as np

from smt.sampling_methods.lhs import LHS


class Configuration:
    pass


class ConfigurationSpace:
    pass


class UniformIntegerHyperparameter:
    pass


def ensure_design_space(xt=None, xlimits=None, design_space=None) -> "BaseDesignSpace":
    """Interface to turn legacy input formats into a DesignSpace"""

    if design_space is not None and isinstance(design_space, BaseDesignSpace):
        return design_space

    if xlimits is not None:
        return DesignSpace(xlimits)

    if xt is not None:
        return DesignSpace([[np.min(xt) - 0.99, np.max(xt) + 1e-4]] * xt.shape[1])

    raise ValueError("Nothing defined that could be interpreted as a design space!")


class DesignVariable:
    """Base class for defining a design variable"""

    upper: Union[float, int]
    lower: Union[float, int]

    def get_typename(self):
        return self.__class__.__name__

    def get_limits(self) -> Union[list, tuple]:
        raise NotImplementedError

    def __str__(self):
        raise NotImplementedError

    def __repr__(self):
        raise NotImplementedError


[docs] class FloatVariable(DesignVariable): """A continuous design variable, varying between its lower and upper bounds""" def __init__(self, lower: float, upper: float): if upper <= lower: raise ValueError( f"Upper bound should be higher than lower bound: {upper} <= {lower}" ) self.lower = lower self.upper = upper def get_limits(self) -> Tuple[float, float]: return self.lower, self.upper def __str__(self): return f"Float ({self.lower}, {self.upper})" def __repr__(self): return f"{self.get_typename()}({self.lower}, {self.upper})"
[docs] class IntegerVariable(DesignVariable): """An integer variable that can take any integer value between the bounds (inclusive)""" def __init__(self, lower: int, upper: int): if upper <= lower: raise ValueError( f"Upper bound should be higher than lower bound: {upper} <= {lower}" ) self.lower = lower self.upper = upper def get_limits(self) -> Tuple[int, int]: return self.lower, self.upper def __str__(self): return f"Int ({self.lower}, {self.upper})" def __repr__(self): return f"{self.get_typename()}({self.lower}, {self.upper})"
[docs] class OrdinalVariable(DesignVariable): """An ordinal variable that can take any of the given value, and where order between the values matters""" def __init__(self, values: List[Union[str, int, float]]): if len(values) < 2: raise ValueError(f"There should at least be 2 values: {values}") self.values = values @property def lower(self) -> int: return 0 @property def upper(self) -> int: return len(self.values) - 1 def get_limits(self) -> List[str]: # We convert to integer strings for compatibility reasons return [str(i) for i in range(len(self.values))] def __str__(self): return f"Ord {self.values}" def __repr__(self): return f"{self.get_typename()}({self.values})"
[docs] class CategoricalVariable(DesignVariable): """A categorical variable that can take any of the given values, and where order does not matter""" def __init__(self, values: List[Union[str, int, float]]): if len(values) < 2: raise ValueError(f"There should at least be 2 values: {values}") self.values = values @property def lower(self) -> int: return 0 @property def upper(self) -> int: return len(self.values) - 1 @property def n_values(self): return len(self.values) def get_limits(self) -> List[Union[str, int, float]]: # We convert to strings for compatibility reasons return [str(value) for value in self.values] def __str__(self): return f"Cat {self.values}" def __repr__(self): return f"{self.get_typename()}({self.values})"
[docs] class BaseDesignSpace: """ Interface for specifying (hierarchical) design spaces. This class itself only specifies the functionality that any design space definition should implement: - a way to specify the design variables, their types, and their bounds or options - a way to correct a set of design vectors such that they satisfy all design space hierarchy constraints - a way to query which design variables are acting for a set of design vectors - a way to impute a set of design vectors such that non-acting design variables are assigned some default value - a way to sample n valid design vectors from the design space If you want to actually define a design space, use the `DesignSpace` class! Note that the correction, querying, and imputation mechanisms should all be implemented in one function (`correct_get_acting`), as usually these operations are tightly related. """ def __init__( self, design_variables: List[DesignVariable] = None, seed=None, ): self._design_variables = design_variables self._is_cat_mask = None self._is_conditionally_acting_mask = None self.seed = np.random.default_rng(seed) self.has_valcons_ord_int = False @property def design_variables(self) -> List[DesignVariable]: if self._design_variables is None: self._design_variables = dvs = self._get_design_variables() if dvs is None: raise RuntimeError( "Design space should either specify the design variables upon initialization " "or as output from _get_design_variables!" ) return self._design_variables @property def is_cat_mask(self) -> np.ndarray: """Boolean mask specifying for each design variable whether it is a categorical variable""" if self._is_cat_mask is None: self._is_cat_mask = np.array( [isinstance(dv, CategoricalVariable) for dv in self.design_variables] ) return self._is_cat_mask @property def is_all_cont(self) -> bool: """Whether or not the space is continuous""" is_continuous = all( isinstance(dv, FloatVariable) for dv in self.design_variables ) return is_continuous @property def is_conditionally_acting(self) -> np.ndarray: """Boolean mask specifying for each design variable whether it is conditionally acting (can be non-acting)""" if self._is_conditionally_acting_mask is None: self._is_conditionally_acting_mask = self._is_conditionally_acting() return self._is_conditionally_acting_mask @property def n_dv(self) -> int: """Get the number of design variables""" return len(self.design_variables) def correct_get_acting(self, x: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """ Correct the given matrix of design vectors and return the corrected vectors and the is_acting matrix. It is automatically detected whether input is provided in unfolded space or not. Parameters ---------- x: np.ndarray [n_obs, dim] - Input variables Returns ------- x_corrected: np.ndarray [n_obs, dim] - Corrected and imputed input variables is_acting: np.ndarray [n_obs, dim] - Boolean matrix specifying for each variable whether it is acting or non-acting """ # Detect whether input is provided in unfolded space x = np.atleast_2d(x) if x.shape[1] == self.n_dv: x_is_unfolded = False elif x.shape[1] == self._get_n_dim_unfolded(): x_is_unfolded = True else: raise ValueError(f"Incorrect shape, expecting {self.n_dv} columns!") # If needed, fold before correcting if x_is_unfolded: x, _ = self.fold_x(x) indi = 0 for i in self.design_variables: if not (isinstance(i, FloatVariable)): x[:, indi] = np.int64(np.round(x[:, indi], 0)) indi += 1 # Correct and get the is_acting matrix x_corrected, is_acting = self._correct_get_acting(x) # Check conditionally-acting status if np.any(~is_acting[:, ~self.is_conditionally_acting]): raise RuntimeError("Unconditionally acting variables cannot be non-acting!") # Unfold if needed if x_is_unfolded: x_corrected, is_acting, _ = self.unfold_x(x_corrected, is_acting) return x_corrected, is_acting def decode_values( self, x: np.ndarray, i_dv: int = None ) -> List[Union[str, int, float, list]]: """ Return decoded values: converts ordinal and categorical back to their original values. If i_dv is given, decoding is done for one specific design variable only. If i_dv=None, decoding will be done for all design variables: 1d input is interpreted as a design vector, 2d input is interpreted as a set of design vectors. """ def _decode_dv(x_encoded: np.ndarray, i_dv_decode): dv = self.design_variables[i_dv_decode] if isinstance(dv, (OrdinalVariable, CategoricalVariable)): values = dv.values decoded_values = [values[int(x_ij)] for x_ij in x_encoded] return decoded_values # No need to decode integer or float variables return list(x_encoded) # Decode one design variable if i_dv is not None: if len(x.shape) == 2: x_i = x[:, i_dv] elif len(x.shape) == 1: x_i = x else: raise ValueError("Expected either 1 or 2-dimensional matrix!") # No need to decode for integer or float variable return _decode_dv(x_i, i_dv_decode=i_dv) # Decode design vectors n_dv = self.n_dv is_1d = len(x.shape) == 1 x_mat = np.atleast_2d(x) if x_mat.shape[1] != n_dv: raise ValueError( f"Incorrect number of inputs, expected {n_dv} design variables, received {x_mat.shape[1]}" ) decoded_des_vars = [_decode_dv(x_mat[:, i], i_dv_decode=i) for i in range(n_dv)] decoded_des_vectors = [ [decoded_des_vars[i][ix] for i in range(n_dv)] for ix in range(x_mat.shape[0]) ] return decoded_des_vectors[0] if is_1d else decoded_des_vectors def sample_valid_x( self, n: int, unfolded=False, seed=None ) -> Tuple[np.ndarray, np.ndarray]: """ Sample n design vectors and additionally return the is_acting matrix. Parameters ---------- n: int - Number of samples to generate unfolded: bool - Whether to return the samples in unfolded space (each categorical level gets its own dimension) seed: int or np.random.Generator - To control random draws Returns ------- x: np.ndarray [n, dim] - Valid design vectors is_acting: np.ndarray [n, dim] - Boolean matrix specifying for each variable whether it is acting or non-acting """ # Sample from the design space x, is_acting = self._sample_valid_x(n, seed=seed) # Check conditionally-acting status if np.any(~is_acting[:, ~self.is_conditionally_acting]): raise RuntimeError("Unconditionally acting variables cannot be non-acting!") # Unfold if needed if unfolded: x, is_acting, _ = self.unfold_x(x, is_acting) return x, is_acting def get_x_limits(self) -> list: """Returns the variable limit definitions in SMT < 2.0 style""" return [dv.get_limits() for dv in self.design_variables] def get_num_bounds(self): """ Get bounds for the design space. Returns ------- np.ndarray [nx, 2] - Bounds of each dimension """ return np.array([(dv.lower, dv.upper) for dv in self.design_variables]) def get_unfolded_num_bounds(self): """ Get bounds for the unfolded continuous space. Returns ------- np.ndarray [nx cont, 2] - Bounds of each dimension where limits for categorical variables are expanded to [0, 1] """ unfolded_x_limits = [] for dv in self.design_variables: if isinstance(dv, CategoricalVariable): unfolded_x_limits += [[0, 1]] * dv.n_values elif isinstance(dv, OrdinalVariable): # Note that this interpretation is slightly different from the original mixed_integer implementation in # smt: we simply map ordinal values to integers, instead of converting them to integer literals # This ensures that each ordinal value gets sampled evenly, also if the values themselves represent # unevenly spaced (e.g. log-spaced) values unfolded_x_limits.append([dv.lower, dv.upper]) else: unfolded_x_limits.append(dv.get_limits()) return np.array(unfolded_x_limits).astype(float) def fold_x( self, x: np.ndarray, is_acting: np.ndarray = None, fold_mask: np.ndarray = None, ) -> Tuple[np.ndarray, Optional[np.ndarray]]: """ Fold x and optionally is_acting. Folding reverses the one-hot encoding of categorical variables applied by unfolding. Parameters ---------- x: np.ndarray [n, dim_unfolded] - Unfolded samples is_acting: np.ndarray [n, dim_unfolded] - Boolean matrix specifying for each unfolded variable whether it is acting or non-acting fold_mask: np.ndarray [dim_folded] - Mask specifying which design variables to apply folding for Returns ------- x_folded: np.ndarray [n, dim] - Folded samples is_acting_folded: np.ndarray [n, dim] - (Optional) boolean matrix specifying for each folded variable whether it is acting or non-acting """ # Get number of unfolded dimension x = np.atleast_2d(x) x_folded = np.zeros((x.shape[0], len(self.design_variables))) is_acting_folded = ( np.ones(x_folded.shape, dtype=bool) if is_acting is not None else None ) i_x_unfold = 0 for i, dv in enumerate(self.design_variables): if (isinstance(dv, CategoricalVariable)) and ( fold_mask is None or fold_mask[i] ): n_dim_cat = dv.n_values # Categorical values are folded by reversed one-hot encoding: # [[1, 0, 0], [0, 1, 0], [0, 0, 1]] --> [0, 1, 2].T x_cat_unfolded = x[:, i_x_unfold : i_x_unfold + n_dim_cat] value_index = np.argmax(x_cat_unfolded, axis=1) x_folded[:, i] = value_index # The is_acting matrix is repeated column-wise, so we can just take the first column if is_acting is not None: is_acting_folded[:, i] = is_acting[:, i_x_unfold] i_x_unfold += n_dim_cat else: x_folded[:, i] = x[:, i_x_unfold] if is_acting is not None: is_acting_folded[:, i] = is_acting[:, i_x_unfold] i_x_unfold += 1 return x_folded, is_acting_folded def unfold_x( self, x: np.ndarray, is_acting: np.ndarray = None, fold_mask: np.ndarray = None ) -> Tuple[np.ndarray, Optional[np.ndarray], Optional[np.ndarray]]: """ Unfold x and optionally is_acting. Unfolding creates one extra dimension for each categorical variable using one-hot encoding. Parameters ---------- x: np.ndarray [n, dim] - Folded samples is_acting: np.ndarray [n, dim] - Boolean matrix specifying for each variable whether it is acting or non-acting fold_mask: np.ndarray [dim_folded] - Mask specifying which design variables to apply folding for Returns ------- x_unfolded: np.ndarray [n, dim_unfolded] - Unfolded samples is_acting_unfolded: np.ndarray [n, dim_unfolded] - (Optional) boolean matrix specifying for each unfolded variable whether it is acting or non-acting is_categorical_unfolded: np.ndarray [n, dim_unfolded] - (Optional) boolean matrix specifying for each unfolded variable whether it is categorical or not """ # Get number of unfolded dimension n_dim_unfolded = self._get_n_dim_unfolded() x = np.atleast_2d(x) x_unfolded = np.zeros((x.shape[0], n_dim_unfolded)) is_acting_unfolded = ( np.ones(x_unfolded.shape, dtype=bool) if is_acting is not None else None ) is_categorical_unfolded = np.ones(x_unfolded.shape, dtype=bool) i_x_unfold = 0 for i, dv in enumerate(self.design_variables): if isinstance(dv, CategoricalVariable) and ( fold_mask is None or fold_mask[i] ): n_dim_cat = dv.n_values x_cat = x_unfolded[:, i_x_unfold : i_x_unfold + n_dim_cat] # Categorical values are unfolded by one-hot encoding: # [0, 1, 2].T --> [[1, 0, 0], [0, 1, 0], [0, 0, 1]] x_i_int = x[:, i].astype(int) for i_level in range(n_dim_cat): has_value_mask = x_i_int == i_level x_cat[has_value_mask, i_level] = 1 # The is_acting matrix is simply repeated column-wise if is_acting is not None: is_acting_unfolded[:, i_x_unfold : i_x_unfold + n_dim_cat] = ( np.tile(is_acting[:, [i]], (1, n_dim_cat)) ) i_x_unfold += n_dim_cat else: x_unfolded[:, i_x_unfold] = x[:, i] if is_acting is not None: is_acting_unfolded[:, i_x_unfold] = is_acting[:, i] is_categorical_unfolded[:, i_x_unfold] *= False i_x_unfold += 1 x_unfolded = x_unfolded[:, :i_x_unfold] if is_acting is not None: is_acting_unfolded = is_acting_unfolded[:, :i_x_unfold] return x_unfolded, is_acting_unfolded, is_categorical_unfolded[0] def _get_n_dim_unfolded(self) -> int: return sum( [ dv.n_values if isinstance(dv, CategoricalVariable) else 1 for dv in self.design_variables ] ) @staticmethod def _round_equally_distributed(x_cont, lower: int, upper: int): """ To ensure equal distribution of continuous values to discrete values, we first stretch-out the continuous values to extend to 0.5 beyond the integer limits and then round. This ensures that the values at the limits get a large-enough share of the continuous values. """ x_cont[x_cont < lower] = lower x_cont[x_cont > upper] = upper diff = upper - lower x_stretched = (x_cont - lower) * ((diff + 0.9999) / (diff + 1e-16)) - 0.5 return np.round(x_stretched) + lower def _to_seed(self, seed=None): seed = None if isinstance(seed, int): seed = seed elif isinstance(seed, np.random.RandomState): seed = seed.get_state()[1][0] return seed
[docs] def _get_design_variables(self) -> List[DesignVariable]: """Return the design variables defined in this design space if not provided upon initialization of the class"""
"""IMPLEMENT FUNCTIONS BELOW"""
[docs] def _is_conditionally_acting(self) -> np.ndarray: """ Return for each design variable whether it is conditionally acting or not. A design variable is conditionally acting if it MAY be non-acting. Returns ------- is_conditionally_acting: np.ndarray [dim] - Boolean vector specifying for each design variable whether it is conditionally acting """ raise NotImplementedError
[docs] def _correct_get_acting(self, x: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """ Correct the given matrix of design vectors and return the corrected vectors and the is_acting matrix. Parameters ---------- x: np.ndarray [n_obs, dim] - Input variables Returns ------- x_corrected: np.ndarray [n_obs, dim] - Corrected and imputed input variables is_acting: np.ndarray [n_obs, dim] - Boolean matrix specifying for each variable whether it is acting or non-acting """ raise NotImplementedError
[docs] def _sample_valid_x(self, n: int, seed=None) -> Tuple[np.ndarray, np.ndarray]: """ Sample n design vectors and additionally return the is_acting matrix. Returns ------- x: np.ndarray [n, dim] - Valid design vectors is_acting: np.ndarray [n, dim] - Boolean matrix specifying for each variable whether it is acting or non-acting seed: int or numpy.random.Generator - A seed to control random draws """ raise NotImplementedError
def __str__(self): raise NotImplementedError def __repr__(self): raise NotImplementedError
VarValueType = Union[int, str, List[Union[int, str]]] def raise_config_space(): raise RuntimeError( "Dependencies are not installed, please install smt_design_space_ext." )
[docs] class DesignSpace(BaseDesignSpace): """ Class for defining a (hierarchical) design space by defining design variables, defining decreed variables (optional), and adding value constraints (optional). Numerical bounds can be requested using `get_num_bounds()`. If needed, it is possible to get the legacy SMT < 2.0 `xlimits` format using `get_x_limits()`. Parameters ---------- design_variables: list[DesignVariable] - The list of design variables: FloatVariable, IntegerVariable, OrdinalVariable, or CategoricalVariable Examples -------- Instantiate the design space with all its design variables: >>> from smt.design_space import * >>> ds = DesignSpace([ >>> CategoricalVariable(['A', 'B']), # x0 categorical: A or B; order is not relevant >>> OrdinalVariable(['C', 'D', 'E']), # x1 ordinal: C, D or E; order is relevant >>> IntegerVariable(0, 2), # x2 integer between 0 and 2 (inclusive): 0, 1, 2 >>> FloatVariable(0, 1), # c3 continuous between 0 and 1 >>> ]) >>> assert len(ds.design_variables) == 4 You can define decreed variables (conditional activation): >>> ds.declare_decreed_var(decreed_var=1, meta_var=0, meta_value='A') # Activate x1 if x0 == A After defining everything correctly, you can then use the design space object to correct design vectors and get information about which design variables are acting: >>> x_corr, is_acting = ds.correct_get_acting(np.array([ >>> [0, 0, 2, .25], >>> [0, 2, 1, .75], >>> ])) >>> assert np.all(x_corr == np.array([ >>> [0, 0, 2, .25], >>> [0, 2, 0, .75], >>> ])) >>> assert np.all(is_acting == np.array([ >>> [True, True, True, True], >>> [True, True, False, True], # x2 is not acting if x1 != C or D (0 or 1) >>> ])) It is also possible to randomly sample design vectors conforming to the constraints: >>> x_sampled, is_acting_sampled = ds.sample_valid_x(100) You can also instantiate a purely-continuous design space from bounds directly: >>> continuous_design_space = DesignSpace([(0, 1), (0, 2), (.5, 5.5)]) >>> assert continuous_design_space.n_dv == 3 If needed, it is possible to get the legacy design space definition format: >>> xlimits = ds.get_x_limits() >>> cont_bounds = ds.get_num_bounds() >>> unfolded_cont_bounds = ds.get_unfolded_num_bounds() """ def __init__( self, design_variables: Union[List[DesignVariable], list, np.ndarray], seed=None, ): self.sampler = None # Assume float variable bounds as inputs def _is_num(val): try: float(val) return True except ValueError: return False if len(design_variables) > 0 and not isinstance( design_variables[0], DesignVariable ): converted_dvs = [] for bounds in design_variables: if len(bounds) != 2 or not _is_num(bounds[0]) or not _is_num(bounds[1]): raise RuntimeError( f"Expecting either a list of DesignVariable objects or float variable " f"bounds! Unrecognized: {bounds!r}" ) converted_dvs.append(FloatVariable(bounds[0], bounds[1])) design_variables = converted_dvs # dict[int, dict[any, list[int]]]: {meta_var_idx: {value: [decreed_var_idx, ...], ...}, ...} self._meta_vars = {} self._is_decreed = np.zeros((len(design_variables),), dtype=bool) super().__init__(design_variables=design_variables, seed=seed)
[docs] def declare_decreed_var( self, decreed_var: int, meta_var: int, meta_value: VarValueType ): """ Define a conditional (decreed) variable to be active when the meta variable has (one of) the provided values. Parameters ---------- decreed_var: int - Index of the conditional variable (the variable that is conditionally active) meta_var: int - Index of the meta variable (the variable that determines whether the conditional var is active) meta_value: int | str | list[int|str] - The value or list of values that the meta variable can have to activate the decreed var """ # Variables cannot be both meta and decreed at the same time if self._is_decreed[meta_var]: raise RuntimeError( f"Variable cannot be both meta and decreed ({meta_var})!" ) # Variables can only be decreed by one meta var if self._is_decreed[decreed_var]: raise RuntimeError(f"Variable is already decreed: {decreed_var}") # Define meta-decreed relationship if meta_var not in self._meta_vars: self._meta_vars[meta_var] = {} meta_var_obj = self.design_variables[meta_var] for value in meta_value if isinstance(meta_value, Sequence) else [meta_value]: encoded_value = value if isinstance(meta_var_obj, (OrdinalVariable, CategoricalVariable)): if value in meta_var_obj.values: encoded_value = meta_var_obj.values.index(value) if encoded_value not in self._meta_vars[meta_var]: self._meta_vars[meta_var][encoded_value] = [] self._meta_vars[meta_var][encoded_value].append(decreed_var) # Mark as decreed (conditionally acting) self._is_decreed[decreed_var] = True
[docs] def add_value_constraint( self, var1: int, value1: VarValueType, var2: int, value2: VarValueType ): """ Define a constraint where two variables cannot have the given values at the same time. Parameters ---------- var1: int - Index of the first variable value1: int | str | list[int|str] - Value or values that the first variable is checked against var2: int - Index of the second variable value2: int | str | list[int|str] - Value or values that the second variable is checked against """ raise_config_space()
def _get_param(self, idx): raise KeyError(f"Variable not found: {idx}") def _get_param2(self, idx): raise KeyError(f"Variable not found: {idx}") @property def _cs_var_idx(self): """ ConfigurationSpace applies topological sort when adding conditions, so compared to what we expect the order of parameters might have changed. This property contains the indices of the params in the ConfigurationSpace. """ raise_config_space() @property def _inv_cs_var_idx(self): """ See _cs_var_idx. This function returns the opposite mapping: the positions of our design variables for each param. """ raise_config_space() def _is_conditionally_acting(self) -> np.ndarray: # Decreed variables are the conditionally acting variables return self._is_decreed def _correct_get_acting(self, x: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """Correct and impute design vectors""" x = x.astype(float) # Simplified implementation # Correct discrete variables x_corr = x.copy() self._normalize_x(x_corr, cs_normalize=False) # Determine which variables are acting is_acting = np.ones(x_corr.shape, dtype=bool) is_acting[:, self._is_decreed] = False for i, xi in enumerate(x_corr): for i_meta, decrees in self._meta_vars.items(): meta_var_value = xi[i_meta] if meta_var_value in decrees: i_decreed_vars = decrees[meta_var_value] is_acting[i, i_decreed_vars] = True # Impute non-acting variables self._impute_non_acting(x_corr, is_acting) return x_corr, is_acting def _sample_valid_x(self, n: int, seed=None) -> Tuple[np.ndarray, np.ndarray]: """Sample design vectors""" # Simplified implementation: sample design vectors in unfolded space x_limits_unfolded = self.get_unfolded_num_bounds() if seed is None: seed = self.seed if self.sampler is None: self.sampler = LHS( xlimits=x_limits_unfolded, seed=seed, criterion="ese", ) x = self.sampler(n) # Fold and cast to discrete x, _ = self.fold_x(x) self._normalize_x(x, cs_normalize=False) # Get acting information and impute return self.correct_get_acting(x) def _get_correct_config(self, vector: np.ndarray) -> Configuration: raise_config_space() def _configs_to_x( self, configs: List["Configuration"] ) -> Tuple[np.ndarray, np.ndarray]: raise_config_space() def _impute_non_acting(self, x: np.ndarray, is_acting: np.ndarray): for i, dv in enumerate(self.design_variables): if isinstance(dv, FloatVariable): # Impute continuous variables to the mid of their bounds x[~is_acting[:, i], i] = 0.5 * (dv.upper - dv.lower) + dv.lower else: # Impute discrete variables to their lower bounds lower = 0 if isinstance(dv, (IntegerVariable, OrdinalVariable)): lower = dv.lower x[~is_acting[:, i], i] = lower def _normalize_x(self, x: np.ndarray, cs_normalize=True): for i, dv in enumerate(self.design_variables): if isinstance(dv, FloatVariable): if cs_normalize: dv.lower = min(np.min(x[:, i]), dv.lower) dv.upper = max(np.max(x[:, i]), dv.upper) x[:, i] = np.clip( (x[:, i] - dv.lower) / (dv.upper - dv.lower + 1e-16), 0, 1 ) elif isinstance(dv, IntegerVariable): x[:, i] = self._round_equally_distributed(x[:, i], dv.lower, dv.upper) if cs_normalize: # After rounding, normalize between 0 and 1, where 0 and 1 represent the stretched bounds x[:, i] = (x[:, i] - dv.lower + 0.49999) / ( dv.upper - dv.lower + 0.9999 ) def _normalize_x_no_integer(self, x: np.ndarray, cs_normalize=True): raise_config_space() def _cs_denormalize_x(self, x: np.ndarray): for i, dv in enumerate(self.design_variables): if isinstance(dv, FloatVariable): x[:, i] = x[:, i] * (dv.upper - dv.lower) + dv.lower elif isinstance(dv, IntegerVariable): # Integer values are normalized similarly to what is done in _round_equally_distributed x[:, i] = np.round( x[:, i] * (dv.upper - dv.lower + 0.9999) + dv.lower - 0.49999 ) def _cs_denormalize_x_ordered(self, x: np.ndarray): ordereddesign_variables = [ self.design_variables[i] for i in self._inv_cs_var_idx ] for i, dv in enumerate(ordereddesign_variables): if isinstance(dv, FloatVariable): x[:, i] = x[:, i] * (dv.upper - dv.lower) + dv.lower elif isinstance(dv, IntegerVariable): # Integer values are normalized similarly to what is done in _round_equally_distributed x[:, i] = np.round( x[:, i] * (dv.upper - dv.lower + 0.9999) + dv.lower - 0.49999 ) def __str__(self): dvs = "\n".join([f"x{i}: {dv!s}" for i, dv in enumerate(self.design_variables)]) return f"Design space:\n{dvs}" def __repr__(self): return f"{self.__class__.__name__}({self.design_variables!r})"