diff --git a/src/ConfigSpace/configuration.py b/src/ConfigSpace/configuration.py index 1b556f7f..bebeab2b 100644 --- a/src/ConfigSpace/configuration.py +++ b/src/ConfigSpace/configuration.py @@ -157,6 +157,17 @@ def check_valid_configuration(self) -> None: """ from ConfigSpace.util import check_configuration + # Verify that the values are legal + for hyperparameter in self.config_space.get_hyperparameters(): + value = self.get(hyperparameter.name, NotSet) + print(hyperparameter) + print(value) + print() + if value is not NotSet and not hyperparameter.legal_value( + value + ): + raise IllegalValueError(hyperparameter, value) + check_configuration( self.config_space, self._vector, diff --git a/src/ConfigSpace/util.py b/src/ConfigSpace/util.py index f93baab4..04e4090d 100644 --- a/src/ConfigSpace/util.py +++ b/src/ConfigSpace/util.py @@ -572,6 +572,15 @@ def check_configuration( # noqa: D103 vector: np.ndarray, allow_inactive_with_values: bool = False, ) -> None: + """Check if a given hyperparameter vector is valid according to the conditionals and forbiddens. + + Raises an Exception if the vector is not valid. + + Args: + space: Configuration space the configuration belongs to. + vector: The hyperparameter vector to check. + allow_inactive_with_values: If True, inactive hyperparameters are allowed to have values in the vector. + """ activated = np.isfinite(vector) # Make sure the roots are all good diff --git a/test/test_configuration_space.py b/test/test_configuration_space.py index 381e3f9c..89ce7d81 100644 --- a/test/test_configuration_space.py +++ b/test/test_configuration_space.py @@ -616,6 +616,42 @@ def test_check_configuration2(): configuration.check_valid_configuration() +def test_check_configuration3(): + # Test that hyperparameters that follow the conditions and forbiddens will still be rejected if out parameter bounds + cs = ConfigurationSpace( + space=[ + NormalIntegerHyperparameter("a", mu=50, sigma=10, lower=1, upper=10), + UniformIntegerHyperparameter("b", lower=1, upper=100), + NormalFloatHyperparameter("c", lower=0, upper=1, mu=0.25, sigma=0.12), + UniformIntegerHyperparameter("d", lower=0, upper=1), + BetaFloatHyperparameter("e", lower=0, upper=1, alpha=1, beta=1), + CategoricalHyperparameter("f", ["Yes", "No", "Maybe"]), + OrdinalHyperparameter("g", ["Low", "Medium", "High"]), + ], + ) + cs.add( + NotEqualsCondition(cs["b"], cs["a"], 10), + ForbiddenEqualsClause(cs["c"], 0), + ForbiddenEqualsClause(cs["d"], 1), + ForbiddenEqualsClause(cs["e"], 0), + ForbiddenEqualsClause(cs["e"], 1), + ForbiddenAndConjunction( + ForbiddenEqualsClause(cs["f"], "No"), + ForbiddenEqualsClause(cs["g"], "Low"), + ), + ) + sample = cs.sample_configuration() + assert sample.check_valid_configuration() is None # Base sample should pass + + # Parameter A has no conditions; check if the configuration fails if a is out of bounds + sample["a"] = 10 + # NOTE: This test currently does not work as adaptive updating of bounds is not merged yet see PR414 + cs["a"].upper = 9 # Adapt upper bound afterwards + + with pytest.raises(IllegalValueError): # Check should fail now + sample.check_valid_configuration() + + def test_check_forbidden_with_sampled_vector_configuration(): cs = ConfigurationSpace() metric = CategoricalHyperparameter("metric", ["minkowski", "other"])