Skip to content

Commit c7e7673

Browse files
Adding new test based on issue
1 parent 57b4c67 commit c7e7673

1 file changed

Lines changed: 138 additions & 1 deletion

File tree

test/test_conditions.py

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@
2727
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2828
from __future__ import annotations
2929

30+
import itertools
31+
3032
import numpy as np
3133
import pytest
3234

33-
from ConfigSpace import ConfigurationSpace
35+
from ConfigSpace import Configuration, ConfigurationSpace
3436
from ConfigSpace.conditions import (
3537
AndConjunction,
3638
EqualsCondition,
@@ -419,3 +421,138 @@ def test_active_hyperparameter():
419421
# This should be the case, as saturation_algorithm is set to "lrs" (which is NOT "inst_gen") in default.
420422
default = cs.get_default_configuration()
421423
cs._check_configuration_rigorous(default)
424+
425+
426+
def test_active_hyperparameter_nested():
427+
# Based on: https://github.com/automl/ConfigSpace/issues/253
428+
# Check that a nested condition does not incorrectly deactivate a parameter
429+
cs = ConfigurationSpace()
430+
x_top = CategoricalHyperparameter("x_top", [0, 1, 2, 3])
431+
432+
x_m0 = CategoricalHyperparameter("x_m0", [0, 1])
433+
x_m1 = CategoricalHyperparameter("x_m1", [0, 1])
434+
x_m2 = CategoricalHyperparameter("x_m2", [0, 1])
435+
436+
y = CategoricalHyperparameter("y", [0, 1])
437+
x_b = CategoricalHyperparameter("x_b", [0, 1])
438+
439+
cm0 = EqualsCondition(x_m0, x_top, 0)
440+
cm1 = EqualsCondition(x_m1, x_top, 1)
441+
cm2 = EqualsCondition(x_m2, x_top, 2)
442+
443+
cb0 = EqualsCondition(x_b, x_top, 0)
444+
cb1 = EqualsCondition(x_b, x_m1, 0)
445+
cb2 = EqualsCondition(x_b, x_m2, 0)
446+
447+
# The resulting nested condition is:
448+
# ((x_b | x_top == 0 || x_b | x_m1 == 0 || x_b | x_m2 == 0) && x_b | y == 0
449+
# Meaning that, for x_b to be active we need:
450+
# either x_top, x_m1 or x_m2 to be 0
451+
# AND y to be 0
452+
#
453+
cor = OrConjunction(cb0, cb1, cb2)
454+
cand = AndConjunction(
455+
cor,
456+
EqualsCondition(x_b, y, 0),
457+
)
458+
459+
cs.add([x_top, x_m0, x_m1, x_b, x_m2, y])
460+
cs.add([cm0, cm1, cm2])
461+
cs.add(cand)
462+
463+
# Create an **illegal** configuration: x_top is equal to three so left side is false eventhough y is equal to 0 (True)
464+
from ConfigSpace import InactiveHyperparameterSetError
465+
466+
cfg = {"y": 0, "x_top": 3, "x_b": 0}
467+
with pytest.raises(InactiveHyperparameterSetError):
468+
cfg = Configuration(cs, values=cfg)
469+
470+
# Now left side is true because x_top is equal to 0 but right side is false because y is equal to 1. Now x_m0 is active because x_top is equal to 0.
471+
cfg = {"y": 1, "x_top": 0, "x_b": 0, "x_m0": 0}
472+
with pytest.raises(InactiveHyperparameterSetError):
473+
cfg = Configuration(cs, values=cfg)
474+
475+
# And now one where x_b is actually active
476+
cfg = {"y": 0, "x_top": 0, "x_b": 0, "x_m0": 0}
477+
cfg = Configuration(cs, values=cfg)
478+
assert cfg.check_valid_configuration() is None
479+
480+
# Second test
481+
# 3 categorical params a = (A, B), b = (C, D), c = (E, F)
482+
# b is active if a == A
483+
# c is active if b == C (and then of course inactive if b is inactive)
484+
# The second condition (for activation of c) can be implemented in two ways:
485+
# 1: Using an EqualsCondition on b == C
486+
# 2: Using an AndConjuction combining the above with the condition a == A
487+
cs = ConfigurationSpace(
488+
name="cs1",
489+
space={
490+
"a": CategoricalHyperparameter("a", ["A", "B"]),
491+
"b": CategoricalHyperparameter("b", ["C", "D"]),
492+
"c": CategoricalHyperparameter("c", ["E", "F"]),
493+
},
494+
)
495+
cs.add(
496+
[
497+
EqualsCondition(cs["b"], cs["a"], "A"), # b is active if a == A
498+
EqualsCondition(
499+
cs["c"],
500+
cs["b"],
501+
"C",
502+
), # c is active if b == C (and b is active)
503+
],
504+
)
505+
506+
# Check that the active hyperparameters are correct
507+
for x in itertools.product([0, 1], [0, 1], [0, 1]):
508+
configuration = Configuration(
509+
cs,
510+
vector=np.array(x),
511+
allow_inactive_with_values=True,
512+
)
513+
x_active = cs.get_active_hyperparameters(configuration)
514+
x_active_should_be = (
515+
{"a"} if x[0] == 1 else ({"a", "b"} if x[1] == 1 else {"a", "b", "c"})
516+
)
517+
try:
518+
assert x_active == x_active_should_be
519+
except AssertionError:
520+
print(
521+
f"{x} ({cs.name}): x_active = {x_active}, whereas it should be {x_active_should_be}",
522+
)
523+
524+
# Second way of specifying nested conditions:
525+
# Child conditions include all ancestors in their condition
526+
cs = ConfigurationSpace(
527+
name="cs2",
528+
space={
529+
"a": CategoricalHyperparameter("a", ["A", "B"]),
530+
"b": CategoricalHyperparameter("b", ["C", "D"]),
531+
"c": CategoricalHyperparameter("c", ["E", "F"]),
532+
},
533+
)
534+
cs.add(
535+
[
536+
EqualsCondition(cs["b"], cs["a"], "A"), # b is active if a == A
537+
# c is active if b == C (and b is active)
538+
AndConjunction(
539+
EqualsCondition(cs["c"], cs["a"], "A"),
540+
EqualsCondition(cs["c"], cs["b"], "C"),
541+
),
542+
],
543+
)
544+
545+
# Check that the active hyperparameters are correct
546+
for x in itertools.product([0, 1], [0, 1], [0, 1]):
547+
x_active = cs.get_active_hyperparameters(
548+
Configuration(cs, vector=np.array(x), allow_inactive_with_values=True),
549+
)
550+
x_active_should_be = (
551+
{"a"} if x[0] == 1 else ({"a", "b"} if x[1] == 1 else {"a", "b", "c"})
552+
)
553+
try:
554+
assert x_active == x_active_should_be
555+
except AssertionError:
556+
print(
557+
f"{x} ({cs.name}): x_active = {x_active}, whereas it should be {x_active_should_be}",
558+
)

0 commit comments

Comments
 (0)