|
27 | 27 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
28 | 28 | from __future__ import annotations |
29 | 29 |
|
| 30 | +import itertools |
| 31 | + |
30 | 32 | import numpy as np |
31 | 33 | import pytest |
32 | 34 |
|
33 | | -from ConfigSpace import ConfigurationSpace |
| 35 | +from ConfigSpace import Configuration, ConfigurationSpace |
34 | 36 | from ConfigSpace.conditions import ( |
35 | 37 | AndConjunction, |
36 | 38 | EqualsCondition, |
@@ -419,3 +421,138 @@ def test_active_hyperparameter(): |
419 | 421 | # This should be the case, as saturation_algorithm is set to "lrs" (which is NOT "inst_gen") in default. |
420 | 422 | default = cs.get_default_configuration() |
421 | 423 | 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