Example for using dependency constraints in discrete searchspaces

This example shows how a dependency constraint can be created for a discrete searchspace. For instance, some parameters might only be relevant when another parameter has a certain value. All dependencies have to be declared in a single constraint.

This example assumes some basic familiarity with using BayBE. We thus refer to campaign for a basic example.

Necessary imports for this example

import os
import numpy as np
from baybe import Campaign
from baybe.constraints import DiscreteDependenciesConstraint, SubSelectionCondition
from baybe.objective import Objective
from baybe.parameters import (
    CategoricalParameter,
    NumericalDiscreteParameter,
    SubstanceParameter,
)
from baybe.searchspace import SearchSpace
from baybe.targets import NumericalTarget
from baybe.utils.dataframe import add_fake_results

Experiment setup

SMOKE_TEST = "SMOKE_TEST" in os.environ
FRAC_RESOLUTION = 3 if SMOKE_TEST else 7
dict_solvent = {
    "water": "O",
    "C1": "C",
}
solvent = SubstanceParameter(name="Solv", data=dict_solvent, encoding="MORDRED")
switch1 = CategoricalParameter(name="Switch1", values=["on", "off"])
switch2 = CategoricalParameter(name="Switch2", values=["left", "right"])
fraction1 = NumericalDiscreteParameter(
    name="Frac1", values=list(np.linspace(0, 100, FRAC_RESOLUTION)), tolerance=0.2
)
frame1 = CategoricalParameter(name="FrameA", values=["A", "B"])
frame2 = CategoricalParameter(name="FrameB", values=["A", "B"])
parameters = [solvent, switch1, switch2, fraction1, frame1, frame2]

Creating the constraints

The constraints are handled when creating the searchspace object. It is thus necessary to define it before the searchspace creation. Note that multiple dependencies have to be included in a single constraint object.

constraint = DiscreteDependenciesConstraint(
    parameters=["Switch1", "Switch2"],
    conditions=[
        SubSelectionCondition(selection=["on"]),
        SubSelectionCondition(selection=["right"]),
    ],
    affected_parameters=[["Solv", "Frac1"], ["FrameA", "FrameB"]],
)

Creating the searchspace and the objective

searchspace = SearchSpace.from_product(parameters=parameters, constraints=[constraint])
objective = Objective(
    mode="SINGLE", targets=[NumericalTarget(name="Target_1", mode="MAX")]
)

Creating and printing the campaign

campaign = Campaign(searchspace=searchspace, objective=objective)
print(campaign)
Campaign
         
 Meta Data
 Batches Done: 0

Fits Done: 0

 Search Space
          
  Search Space Type: DISCRETE
  
  Discrete Search Space
               
   Discrete Parameters
         Name                        Type  Num_Values                   Encoding
   0     Solv          SubstanceParameter           2  SubstanceEncoding.MORDRED
   1  Switch1        CategoricalParameter           2    CategoricalEncoding.OHE
   2  Switch2        CategoricalParameter           2    CategoricalEncoding.OHE
   3    Frac1  NumericalDiscreteParameter           3                       None
   4   FrameA        CategoricalParameter           2    CategoricalEncoding.OHE
   5   FrameB        CategoricalParameter           2    CategoricalEncoding.OHE
               
   Experimental Representation
               
    Solv Switch1  ... FrameA  FrameB
   0   water      on  ...      A       A
   1   water      on  ...      A       A
   2   water      on  ...      A       A
   ..    ...     ...  ...    ...     ...
   32     C1      on  ...      A       B
   33     C1      on  ...      B       A
   34     C1      on  ...      B       B
   
   [35 rows x 6 columns]
   
   Metadata:

was_recommended: 0/35

was_measured: 0/35

dont_recommend: 0/35

   Constraints
                                Type Affected_Parameters
   0  DiscreteDependenciesConstraint  [Switch1, Switch2]
               
   Computational Representation
               
   Solv_MORDRED_nAtom  Switch1_on  ...  FrameB_A  FrameB_B
   0                  3.0           1  ...         1         0
   1                  3.0           1  ...         1         0
   2                  3.0           1  ...         1         0
   ..                 ...         ...  ...       ...       ...
   32                 5.0           1  ...         0         1
   33                 5.0           1  ...         1         0
   34                 5.0           1  ...         0         1
   
   [35 rows x 10 columns]
 
 Objective
          
  Mode: SINGLE
          
  Targets 
                Type      Name Mode  Lower_Bound  Upper_Bound Transformation  \
  0  NumericalTarget  Target_1  MAX         -inf          inf           None   
  
     Weight  
  0   100.0  
          
  Combine Function: GEOM_MEAN
 
 TwoPhaseMetaRecommender(allow_repeated_recommendations=None,

allow_recommending_already_measured=None, initial_recommender=RandomRecommender(allow_repeated_recommendations=False, allow_recommending_already_measured=True), recommender=SequentialGreedyRecommender(allow_repeated_recommendations=False, allow_recommending_already_measured=True, surrogate_model=GaussianProcessSurrogate(model_params={}, _model=None), acquisition_function_cls=’qEI’, _acquisition_function=None, hybrid_sampler=’None’, sampling_percentage=1.0), switch_after=1)

Manual verification of the constraints

The following loop performs some recommendations and manually verifies the given constraints.

N_ITERATIONS = 2 if SMOKE_TEST else 5
for kIter in range(N_ITERATIONS):
    print(f"\n#### ITERATION {kIter+1} ####")

    print("## ASSERTS ##")
    print(
        f"Number entries with both switches on "
        f"(expected {7*len(dict_solvent)*2*2}): ",
        (
            (campaign.searchspace.discrete.exp_rep["Switch1"] == "on")
            & (campaign.searchspace.discrete.exp_rep["Switch2"] == "right")
        ).sum(),
    )
    print(
        f"Number entries with Switch1 off " f"(expected {2*2}):       ",
        (
            (campaign.searchspace.discrete.exp_rep["Switch1"] == "off")
            & (campaign.searchspace.discrete.exp_rep["Switch2"] == "right")
        ).sum(),
    )
    print(
        f"Number entries with Switch2 off "
        f"(expected {7*len(dict_solvent)}):"
        f"      ",
        (
            (campaign.searchspace.discrete.exp_rep["Switch1"] == "on")
            & (campaign.searchspace.discrete.exp_rep["Switch2"] == "left")
        ).sum(),
    )
    print(
        "Number entries with both switches off (expected 1): ",
        (
            (campaign.searchspace.discrete.exp_rep["Switch1"] == "off")
            & (campaign.searchspace.discrete.exp_rep["Switch2"] == "left")
        ).sum(),
    )

    rec = campaign.recommend(batch_size=5)
    add_fake_results(rec, campaign)
    campaign.add_measurements(rec)
#### ITERATION 1 ####
## ASSERTS ##
Number entries with both switches on (expected 56):  24
Number entries with Switch1 off (expected 4):        4
Number entries with Switch2 off (expected 14):       6
Number entries with both switches off (expected 1):  1

#### ITERATION 2 ####
## ASSERTS ##
Number entries with both switches on (expected 56):  24
Number entries with Switch1 off (expected 4):        4
Number entries with Switch2 off (expected 14):       6
Number entries with both switches off (expected 1):  1