Skip to content

Constraints

categorical

CategoricalExcludeConstraint

Bases: Constraint

Class for modelling exclusion constraints.

It evaluates conditions on two features and combines them using logical operators. If the logical combination evaluates to true, the constraint is not fulfilled. So far, this kind of constraint is only supported by the RandomStrategy.

Attributes:

Name Type Description
features FeatureKeys

List of feature keys to apply the conditions on.

conditions Annotated[List[Union[ThresholdCondition, SelectionCondition]], Field(min_length=2, max_length=2)]

List of conditions to evaluate.

logical_op Literal['AND', 'OR', 'XOR']

Logical operator to combine the conditions. Can be "AND", "OR", or "XOR". Default is "AND".

Source code in bofire/data_models/constraints/categorical.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
class CategoricalExcludeConstraint(Constraint):
    """Class for modelling exclusion constraints.

    It evaluates conditions on two features and combines them using logical operators.
    If the logical combination evaluates to true, the constraint is not fulfilled.
    So far, this kind of constraint is only supported by the RandomStrategy.

    Attributes:
        features: List of feature keys to apply the conditions on.
        conditions: List of conditions to evaluate.
        logical_op: Logical operator to combine the conditions. Can be "AND", "OR", or "XOR".
            Default is "AND".
    """

    type: Literal["CategoricalExcludeConstraint"] = "CategoricalExcludeConstraint"
    features: FeatureKeys
    conditions: Annotated[
        List[Union[ThresholdCondition, SelectionCondition]],
        Field(min_length=2, max_length=2),
    ]
    logical_op: Literal["AND", "OR", "XOR"] = "AND"

    def validate_inputs(self, inputs: Inputs):
        """Validates that the features stored in Inputs are compatible with the constraint.

        Args:
            inputs: Inputs to validate.
        """
        found_categorical = False
        keys = inputs.get_keys([CategoricalInput, DiscreteInput, ContinuousInput])
        for f in self.features:
            if f not in keys:
                raise ValueError(
                    f"Feature {f} is not a input feature in the provided Inputs object.",
                )
        for i in range(2):
            feat = inputs.get_by_key(self.features[i])
            condition = self.conditions[i]
            if isinstance(feat, CategoricalInput):
                found_categorical = True
                if not isinstance(condition, SelectionCondition):
                    raise ValueError(
                        f"Condition for feature {self.features[i]} is not a SubSelectionCondition.",
                    )
                if not all(key in feat.categories for key in condition.selection):
                    raise ValueError(
                        f"Some categories in condition {i} are not valid categories for feature {self.features[i]}."
                    )
            elif isinstance(feat, DiscreteInput):
                if isinstance(condition, SelectionCondition):
                    if not all(key in feat.values for key in condition.selection):
                        raise ValueError(
                            f"Some values in condition {i} are not valid values for feature {self.features[i]}."
                        )
            else:  # we have a ContinuousInput
                if not isinstance(condition, ThresholdCondition):
                    raise ValueError(
                        f"Condition for ContinuousInput {self.features[i]} is not a ThresholdCondition.",
                    )
        if not found_categorical:
            raise ValueError(
                "At least one of the features must be a CategoricalInput feature.",
            )

    def __call__(self, experiments: pd.DataFrame) -> pd.Series:
        """Numerically evaluates the constraint.

        Returns the distance to the constraint fulfillment. Here 1 for not fulfilled
        and 0 for fulfilled.

        Args:
            experiments (pd.DataFrame): Dataframe to evaluate the constraint on.

        Returns:
            pd.Series: Distance to reach constraint fulfillment.

        """
        return self.is_fulfilled(experiments).astype(float)

    def is_fulfilled(
        self,
        experiments: pd.DataFrame,
        tol: Optional[float] = 1e-6,
    ) -> pd.Series:
        """Checks if the constraint is fulfilled for the given experiments.

        Args:
            experiments DataFrame containing the experiments.
            tol: Tolerance for checking. Not used here. Defaults to 1e-6.

        Returns:
            Series indicating whether the constraint is fulfilled for each experiment.

        """
        fulfilled_conditions = [
            condition(experiments[self.features[i]])
            for i, condition in enumerate(self.conditions)
        ]

        if self.logical_op == "AND":
            return ~(fulfilled_conditions[0] & fulfilled_conditions[1])
        elif self.logical_op == "OR":
            return ~(fulfilled_conditions[0] | fulfilled_conditions[1])
        else:
            return ~(fulfilled_conditions[0] ^ fulfilled_conditions[1])

    def jacobian(self, experiments: pd.DataFrame) -> pd.DataFrame:
        raise NotImplementedError("Method `jacobian` currently not implemented.")

    def hessian(self, experiments: pd.DataFrame) -> Dict[Union[str, int], pd.DataFrame]:
        raise NotImplementedError("Method `hessian` currently not implemented.")

__call__(experiments)

Numerically evaluates the constraint.

Returns the distance to the constraint fulfillment. Here 1 for not fulfilled and 0 for fulfilled.

Parameters:

Name Type Description Default
experiments DataFrame

Dataframe to evaluate the constraint on.

required

Returns:

Type Description
Series

pd.Series: Distance to reach constraint fulfillment.

Source code in bofire/data_models/constraints/categorical.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def __call__(self, experiments: pd.DataFrame) -> pd.Series:
    """Numerically evaluates the constraint.

    Returns the distance to the constraint fulfillment. Here 1 for not fulfilled
    and 0 for fulfilled.

    Args:
        experiments (pd.DataFrame): Dataframe to evaluate the constraint on.

    Returns:
        pd.Series: Distance to reach constraint fulfillment.

    """
    return self.is_fulfilled(experiments).astype(float)

is_fulfilled(experiments, tol=1e-06)

Checks if the constraint is fulfilled for the given experiments.

Parameters:

Name Type Description Default
tol Optional[float]

Tolerance for checking. Not used here. Defaults to 1e-6.

1e-06

Returns:

Type Description
Series

Series indicating whether the constraint is fulfilled for each experiment.

Source code in bofire/data_models/constraints/categorical.py
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def is_fulfilled(
    self,
    experiments: pd.DataFrame,
    tol: Optional[float] = 1e-6,
) -> pd.Series:
    """Checks if the constraint is fulfilled for the given experiments.

    Args:
        experiments DataFrame containing the experiments.
        tol: Tolerance for checking. Not used here. Defaults to 1e-6.

    Returns:
        Series indicating whether the constraint is fulfilled for each experiment.

    """
    fulfilled_conditions = [
        condition(experiments[self.features[i]])
        for i, condition in enumerate(self.conditions)
    ]

    if self.logical_op == "AND":
        return ~(fulfilled_conditions[0] & fulfilled_conditions[1])
    elif self.logical_op == "OR":
        return ~(fulfilled_conditions[0] | fulfilled_conditions[1])
    else:
        return ~(fulfilled_conditions[0] ^ fulfilled_conditions[1])

validate_inputs(inputs)

Validates that the features stored in Inputs are compatible with the constraint.

Parameters:

Name Type Description Default
inputs Inputs

Inputs to validate.

required
Source code in bofire/data_models/constraints/categorical.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def validate_inputs(self, inputs: Inputs):
    """Validates that the features stored in Inputs are compatible with the constraint.

    Args:
        inputs: Inputs to validate.
    """
    found_categorical = False
    keys = inputs.get_keys([CategoricalInput, DiscreteInput, ContinuousInput])
    for f in self.features:
        if f not in keys:
            raise ValueError(
                f"Feature {f} is not a input feature in the provided Inputs object.",
            )
    for i in range(2):
        feat = inputs.get_by_key(self.features[i])
        condition = self.conditions[i]
        if isinstance(feat, CategoricalInput):
            found_categorical = True
            if not isinstance(condition, SelectionCondition):
                raise ValueError(
                    f"Condition for feature {self.features[i]} is not a SubSelectionCondition.",
                )
            if not all(key in feat.categories for key in condition.selection):
                raise ValueError(
                    f"Some categories in condition {i} are not valid categories for feature {self.features[i]}."
                )
        elif isinstance(feat, DiscreteInput):
            if isinstance(condition, SelectionCondition):
                if not all(key in feat.values for key in condition.selection):
                    raise ValueError(
                        f"Some values in condition {i} are not valid values for feature {self.features[i]}."
                    )
        else:  # we have a ContinuousInput
            if not isinstance(condition, ThresholdCondition):
                raise ValueError(
                    f"Condition for ContinuousInput {self.features[i]} is not a ThresholdCondition.",
                )
    if not found_categorical:
        raise ValueError(
            "At least one of the features must be a CategoricalInput feature.",
        )

Condition

Bases: BaseModel

Base class for all conditions.

Conditions evaluate an expression regarding a single feature. Conditions are part of the CategoricalExcludeConstraint.

Source code in bofire/data_models/constraints/categorical.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Condition(BaseModel):
    """Base class for all conditions.

    Conditions evaluate an expression regarding a single feature.
    Conditions are part of the CategoricalExcludeConstraint.
    """

    type: Any

    @abstractmethod
    def __call__(self, values: pd.Series) -> pd.Series:
        """Evaluate the condition on a given data series.

        Args:
            values: A series containing feature values.

        Returns:
            A Boolean series indicating which elements satisfy the condition.
        """

__call__(values) abstractmethod

Evaluate the condition on a given data series.

Parameters:

Name Type Description Default
values Series

A series containing feature values.

required

Returns:

Type Description
Series

A Boolean series indicating which elements satisfy the condition.

Source code in bofire/data_models/constraints/categorical.py
37
38
39
40
41
42
43
44
45
46
@abstractmethod
def __call__(self, values: pd.Series) -> pd.Series:
    """Evaluate the condition on a given data series.

    Args:
        values: A series containing feature values.

    Returns:
        A Boolean series indicating which elements satisfy the condition.
    """

SelectionCondition

Bases: Condition

Class for modelling selection conditions.

It is checked if the feature value is in the selection of values. If this is the case, the condition is fulfilled. It can be only applied to CategoricalInput and DiscreteInput features.

Attributes:

Name Type Description
selection List[Union[str, float, int]]

In case of CategoricalInput, the selection of categories to be included. In case of DiscreteInput, the selection of values to be included.

Source code in bofire/data_models/constraints/categorical.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
class SelectionCondition(Condition):
    """Class for modelling selection conditions.

    It is checked if the feature value is in the selection of values. If this is the case,
    the condition is fulfilled. It can be only applied to CategoricalInput and DiscreteInput
    features.

    Attributes:
        selection: In case of CategoricalInput, the selection of categories to be included.
            In case of DiscreteInput, the selection of values to be included.
    """

    type: Literal["SelectionCondition"] = "SelectionCondition"

    selection: List[Union[str, float, int]]

    def __call__(self, values: pd.Series) -> pd.Series:
        return values.isin(self.selection)

ThresholdCondition

Bases: Condition

Class for modelling threshold conditions.

It can only be applied to ContinuousInput and DiscreteInput features. It is checked if the feature value is above or below a certain threshold depending on the operator. If the expression evaluated to true, the condition is fulfilled.

Attributes:

Name Type Description
threshold float

Threshold value.

operator Literal['<', '<=', '>', '>=']

Operator to use for comparison. Can be one of "<", "<=", ">", ">=".

Source code in bofire/data_models/constraints/categorical.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class ThresholdCondition(Condition):
    """Class for modelling threshold conditions.

    It can only be applied to ContinuousInput and DiscreteInput features. It is
    checked if the feature value is above or below a certain threshold depending on the
    operator. If the expression evaluated to true, the condition is fulfilled.

    Attributes:
        threshold: Threshold value.
        operator: Operator to use for comparison. Can be one of "<", "<=", ">", ">=".
    """

    type: Literal["ThresholdCondition"] = "ThresholdCondition"
    threshold: float
    operator: Literal["<", "<=", ">", ">="]

    def __call__(self, values: pd.Series) -> pd.Series:
        def evaluate(x: ArrayLike):
            return _threshold_operators[self.operator](x, self.threshold)

        return values.apply(evaluate)

constraint

Constraint

Bases: BaseModel

Abstract base class to define constraints on the optimization space.

Source code in bofire/data_models/constraints/constraint.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class Constraint(BaseModel):
    """Abstract base class to define constraints on the optimization space."""

    type: Any
    features: FeatureKeys

    @abstractmethod
    def is_fulfilled(
        self,
        experiments: pd.DataFrame,
        tol: Optional[float] = 1e-6,
    ) -> pd.Series:
        """Abstract method to check if a constraint is fulfilled for all the rows of the provided dataframe.

        Args:
            experiments (pd.DataFrame): Dataframe to check constraint fulfillment.
            tol (float, optional): tolerance parameter. A constraint is considered as not fulfilled if
                the violation is larger than tol. Defaults to 0.

        Returns:
            bool: True if fulfilled else False

        """

    @abstractmethod
    def __call__(self, experiments: pd.DataFrame) -> pd.Series:
        """Numerically evaluates the constraint.

        Args:
            experiments (pd.DataFrame): Dataframe to evaluate the constraint on.

        Returns:
            pd.Series: Distance to reach constraint fulfillment.

        """

    @abstractmethod
    def jacobian(self, experiments: pd.DataFrame) -> pd.DataFrame:
        """Numerically evaluates the jacobian of the constraint
        Args:
            experiments (pd.DataFrame): Dataframe to evaluate the constraint on.

        Returns:
            pd.DataFrame: the i-th row contains the jacobian evaluated at the i-th experiment

        """

    @abstractmethod
    def validate_inputs(self, inputs: Inputs):
        """Validates that the features stored in Inputs are compatible with the constraint.

        Args:
            inputs (Inputs): Inputs to validate.

        """

__call__(experiments) abstractmethod

Numerically evaluates the constraint.

Parameters:

Name Type Description Default
experiments DataFrame

Dataframe to evaluate the constraint on.

required

Returns:

Type Description
Series

pd.Series: Distance to reach constraint fulfillment.

Source code in bofire/data_models/constraints/constraint.py
36
37
38
39
40
41
42
43
44
45
46
@abstractmethod
def __call__(self, experiments: pd.DataFrame) -> pd.Series:
    """Numerically evaluates the constraint.

    Args:
        experiments (pd.DataFrame): Dataframe to evaluate the constraint on.

    Returns:
        pd.Series: Distance to reach constraint fulfillment.

    """

is_fulfilled(experiments, tol=1e-06) abstractmethod

Abstract method to check if a constraint is fulfilled for all the rows of the provided dataframe.

Parameters:

Name Type Description Default
experiments DataFrame

Dataframe to check constraint fulfillment.

required
tol float

tolerance parameter. A constraint is considered as not fulfilled if the violation is larger than tol. Defaults to 0.

1e-06

Returns:

Name Type Description
bool Series

True if fulfilled else False

Source code in bofire/data_models/constraints/constraint.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@abstractmethod
def is_fulfilled(
    self,
    experiments: pd.DataFrame,
    tol: Optional[float] = 1e-6,
) -> pd.Series:
    """Abstract method to check if a constraint is fulfilled for all the rows of the provided dataframe.

    Args:
        experiments (pd.DataFrame): Dataframe to check constraint fulfillment.
        tol (float, optional): tolerance parameter. A constraint is considered as not fulfilled if
            the violation is larger than tol. Defaults to 0.

    Returns:
        bool: True if fulfilled else False

    """

jacobian(experiments) abstractmethod

Numerically evaluates the jacobian of the constraint Args: experiments (pd.DataFrame): Dataframe to evaluate the constraint on.

Returns:

Type Description
DataFrame

pd.DataFrame: the i-th row contains the jacobian evaluated at the i-th experiment

Source code in bofire/data_models/constraints/constraint.py
48
49
50
51
52
53
54
55
56
57
@abstractmethod
def jacobian(self, experiments: pd.DataFrame) -> pd.DataFrame:
    """Numerically evaluates the jacobian of the constraint
    Args:
        experiments (pd.DataFrame): Dataframe to evaluate the constraint on.

    Returns:
        pd.DataFrame: the i-th row contains the jacobian evaluated at the i-th experiment

    """

validate_inputs(inputs) abstractmethod

Validates that the features stored in Inputs are compatible with the constraint.

Parameters:

Name Type Description Default
inputs Inputs

Inputs to validate.

required
Source code in bofire/data_models/constraints/constraint.py
59
60
61
62
63
64
65
66
@abstractmethod
def validate_inputs(self, inputs: Inputs):
    """Validates that the features stored in Inputs are compatible with the constraint.

    Args:
        inputs (Inputs): Inputs to validate.

    """

ConstraintError

Bases: Exception

Base Error for Constraints

Source code in bofire/data_models/constraints/constraint.py
94
95
class ConstraintError(Exception):
    """Base Error for Constraints"""

ConstraintNotFulfilledError

Bases: ConstraintError

Raised when an constraint is not fulfilled.

Source code in bofire/data_models/constraints/constraint.py
98
99
class ConstraintNotFulfilledError(ConstraintError):
    """Raised when an constraint is not fulfilled."""

IntrapointConstraint

Bases: Constraint

An intrapoint constraint describes required relationships within a candidate when asking a strategy to return one or more candidates.

Source code in bofire/data_models/constraints/constraint.py
69
70
71
72
73
74
class IntrapointConstraint(Constraint):
    """An intrapoint constraint describes required relationships within a candidate
    when asking a strategy to return one or more candidates.
    """

    type: str

interpoint

InterpointConstraint

Bases: Constraint

An interpoint constraint describes required relationships between individual candidates when asking a strategy for returning more than one candidate.

Source code in bofire/data_models/constraints/interpoint.py
13
14
15
16
17
18
class InterpointConstraint(Constraint):
    """An interpoint constraint describes required relationships between individual
    candidates when asking a strategy for returning more than one candidate.
    """

    type: str

InterpointEqualityConstraint

Bases: InterpointConstraint

Constraint that forces that values of a certain feature of a set/batch of candidates should have the same value. Currently this is only implemented for ContinuousInput features.

Attributes:

Name Type Description
feature(str)

The constrained feature.

multiplicity(int)

The multiplicity of the constraint, stating how many values of the feature in the batch should have always the same value.

Source code in bofire/data_models/constraints/interpoint.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
class InterpointEqualityConstraint(InterpointConstraint):
    """Constraint that forces that values of a certain feature of a set/batch of
    candidates should have the same value. Currently this is only implemented for ContinuousInput features.

    Attributes:
        feature(str): The constrained feature.
        multiplicity(int): The multiplicity of the constraint, stating how many
            values of the feature in the batch should have always the same value.

    """

    type: Literal["InterpointEqualityConstraint"] = "InterpointEqualityConstraint"
    features: Annotated[List[str], Field(min_length=1), Field(max_length=1)]
    multiplicity: Optional[Annotated[int, Field(ge=2)]] = None

    @property
    def feature(self) -> str:
        """Feature to be constrained."""
        return self.features[0]

    def validate_inputs(self, inputs: Inputs):
        if self.feature not in inputs.get_keys(ContinuousInput):
            raise ValueError(
                f"Feature {self.feature} is not a continuous input feature in the provided Inputs object.",
            )

    def is_fulfilled(
        self,
        experiments: pd.DataFrame,
        tol: Optional[float] = 1e-6,
    ) -> pd.Series:
        multiplicity = self.multiplicity or len(experiments)
        for i in range(math.ceil(len(experiments) / multiplicity)):
            batch = experiments[self.feature].values[
                i * multiplicity : min((i + 1) * multiplicity, len(experiments))
            ]
            if not np.allclose(batch, batch[0]):
                return pd.Series([False])
        return pd.Series([True])

    def __call__(self, experiments: pd.DataFrame) -> pd.Series:
        """Numerically evaluates the constraint. Returns the distance to the constraint fulfillment
        for each batch of size batch_size.

        Args:
            experiments (pd.DataFrame): Dataframe to evaluate the constraint on.

        Returns:
            pd.Series: Distance to reach constraint fulfillment.

        """
        multiplicity = self.multiplicity or len(experiments)
        n_batches = int(np.ceil(experiments.shape[0] / multiplicity))
        feature_values = np.zeros(n_batches * multiplicity)
        feature_values[: experiments.shape[0]] = experiments[self.feature].values
        feature_values[experiments.shape[0] :] = feature_values[-multiplicity]
        feature_values = feature_values.reshape(n_batches, multiplicity).T

        batchwise_constraint_matrix = np.zeros(shape=(multiplicity - 1, multiplicity))
        batchwise_constraint_matrix[:, 0] = 1.0
        batchwise_constraint_matrix[:, 1:] = -np.eye(multiplicity - 1)

        return pd.Series(
            np.linalg.norm(batchwise_constraint_matrix @ feature_values, axis=0, ord=2)
            ** 2,
            index=[f"batch_{i}" for i in range(n_batches)],
        )

    def jacobian(self, experiments: pd.DataFrame) -> pd.DataFrame:
        raise NotImplementedError("Method `jacobian` currently not implemented.")

    def hessian(self, experiments: pd.DataFrame) -> Dict[Union[str, int], pd.DataFrame]:
        raise NotImplementedError("Method `hessian` currently not implemented.")

feature property

Feature to be constrained.

__call__(experiments)

Numerically evaluates the constraint. Returns the distance to the constraint fulfillment for each batch of size batch_size.

Parameters:

Name Type Description Default
experiments DataFrame

Dataframe to evaluate the constraint on.

required

Returns:

Type Description
Series

pd.Series: Distance to reach constraint fulfillment.

Source code in bofire/data_models/constraints/interpoint.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def __call__(self, experiments: pd.DataFrame) -> pd.Series:
    """Numerically evaluates the constraint. Returns the distance to the constraint fulfillment
    for each batch of size batch_size.

    Args:
        experiments (pd.DataFrame): Dataframe to evaluate the constraint on.

    Returns:
        pd.Series: Distance to reach constraint fulfillment.

    """
    multiplicity = self.multiplicity or len(experiments)
    n_batches = int(np.ceil(experiments.shape[0] / multiplicity))
    feature_values = np.zeros(n_batches * multiplicity)
    feature_values[: experiments.shape[0]] = experiments[self.feature].values
    feature_values[experiments.shape[0] :] = feature_values[-multiplicity]
    feature_values = feature_values.reshape(n_batches, multiplicity).T

    batchwise_constraint_matrix = np.zeros(shape=(multiplicity - 1, multiplicity))
    batchwise_constraint_matrix[:, 0] = 1.0
    batchwise_constraint_matrix[:, 1:] = -np.eye(multiplicity - 1)

    return pd.Series(
        np.linalg.norm(batchwise_constraint_matrix @ feature_values, axis=0, ord=2)
        ** 2,
        index=[f"batch_{i}" for i in range(n_batches)],
    )

linear

LinearConstraint

Bases: IntrapointConstraint

Abstract base class for linear equality and inequality constraints.

Attributes:

Name Type Description
features list

list of feature keys (str) on which the constraint works on.

coefficients list

list of coefficients (float) of the constraint.

rhs float

Right-hand side of the constraint

Source code in bofire/data_models/constraints/linear.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class LinearConstraint(IntrapointConstraint):
    """Abstract base class for linear equality and inequality constraints.

    Attributes:
        features (list): list of feature keys (str) on which the constraint works on.
        coefficients (list): list of coefficients (float) of the constraint.
        rhs (float): Right-hand side of the constraint

    """

    type: Literal["LinearConstraint"] = "LinearConstraint"

    coefficients: Annotated[List[float], Field(min_length=2)]
    rhs: float

    @model_validator(mode="after")
    def validate_list_lengths(self):
        """Validate that length of the feature and coefficient lists have the same length."""
        if len(self.features) != len(self.coefficients):
            raise ValueError(
                f"must provide same number of features and coefficients, got {len(self.features)} != {len(self.coefficients)}",
            )
        return self

    def validate_inputs(self, inputs: Inputs):
        keys = inputs.get_keys([ContinuousInput, DiscreteInput])
        for f in self.features:
            if f not in keys:
                raise ValueError(
                    f"Feature {f} is not a continuous input feature in the provided Inputs object.",
                )

    def __call__(self, experiments: pd.DataFrame) -> pd.Series:
        return (
            experiments[self.features] @ self.coefficients - self.rhs
        ) / np.linalg.norm(np.array(self.coefficients))

    def jacobian(self, experiments: pd.DataFrame) -> pd.DataFrame:
        return pd.DataFrame(
            np.tile(
                [
                    np.array(self.coefficients)
                    / np.linalg.norm(np.array(self.coefficients)),
                ],
                [experiments.shape[0], 1],
            ),
            columns=[f"dg/d{name}" for name in self.features],
        )

    def hessian(self, experiments: pd.DataFrame) -> Dict[Union[int, str], float]:
        return dict.fromkeys(range(experiments.shape[0]), 0.0)

validate_list_lengths()

Validate that length of the feature and coefficient lists have the same length.

Source code in bofire/data_models/constraints/linear.py
31
32
33
34
35
36
37
38
@model_validator(mode="after")
def validate_list_lengths(self):
    """Validate that length of the feature and coefficient lists have the same length."""
    if len(self.features) != len(self.coefficients):
        raise ValueError(
            f"must provide same number of features and coefficients, got {len(self.features)} != {len(self.coefficients)}",
        )
    return self

LinearEqualityConstraint

Bases: LinearConstraint, EqualityConstraint

Linear equality constraint of the form coefficients * x = rhs.

Attributes:

Name Type Description
features list

list of feature keys (str) on which the constraint works on.

coefficients list

list of coefficients (float) of the constraint.

rhs float

Right-hand side of the constraint

Source code in bofire/data_models/constraints/linear.py
69
70
71
72
73
74
75
76
77
78
79
class LinearEqualityConstraint(LinearConstraint, EqualityConstraint):
    """Linear equality constraint of the form `coefficients * x = rhs`.

    Attributes:
        features (list): list of feature keys (str) on which the constraint works on.
        coefficients (list): list of coefficients (float) of the constraint.
        rhs (float): Right-hand side of the constraint

    """

    type: Literal["LinearEqualityConstraint"] = "LinearEqualityConstraint"

LinearInequalityConstraint

Bases: LinearConstraint, InequalityConstraint

Linear inequality constraint of the form coefficients * x <= rhs.

To instantiate a constraint of the form coefficients * x >= rhs multiply coefficients and rhs by -1, or use the classmethod from_greater_equal.

Attributes:

Name Type Description
features list

list of feature keys (str) on which the constraint works on.

coefficients list

list of coefficients (float) of the constraint.

rhs float

Right-hand side of the constraint

Source code in bofire/data_models/constraints/linear.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
class LinearInequalityConstraint(LinearConstraint, InequalityConstraint):
    """Linear inequality constraint of the form `coefficients * x <= rhs`.

    To instantiate a constraint of the form `coefficients * x >= rhs` multiply coefficients and rhs by -1, or
    use the classmethod `from_greater_equal`.

    Attributes:
        features (list): list of feature keys (str) on which the constraint works on.
        coefficients (list): list of coefficients (float) of the constraint.
        rhs (float): Right-hand side of the constraint

    """

    type: Literal["LinearInequalityConstraint"] = "LinearInequalityConstraint"

    def as_smaller_equal(self) -> Tuple[List[str], List[float], float]:
        """Return attributes in the smaller equal convention

        Returns:
            Tuple[List[str], List[float], float]: features, coefficients, rhs

        """
        return self.features, self.coefficients, self.rhs

    def as_greater_equal(self) -> Tuple[List[str], List[float], float]:
        """Return attributes in the greater equal convention

        Returns:
            Tuple[List[str], List[float], float]: features, coefficients, rhs

        """
        return self.features, [-1.0 * c for c in self.coefficients], -1.0 * self.rhs

    @classmethod
    def from_greater_equal(
        cls,
        features: List[str],
        coefficients: List[float],
        rhs: float,
    ):
        """Class method to construct linear inequality constraint of the form `coefficients * x >= rhs`.

        Args:
            features (List[str]): List of feature keys.
            coefficients (List[float]): List of coefficients.
            rhs (float): Right-hand side of the constraint.

        """
        return cls(
            features=features,
            coefficients=[-1.0 * c for c in coefficients],
            rhs=-1.0 * rhs,
        )

    @classmethod
    def from_smaller_equal(
        cls,
        features: List[str],
        coefficients: List[float],
        rhs: float,
    ):
        """Class method to construct linear inequality constraint of the form `coefficients * x <= rhs`.

        Args:
            features (List[str]): List of feature keys.
            coefficients (List[float]): List of coefficients.
            rhs (float): Right-hand side of the constraint.

        """
        return cls(
            features=features,
            coefficients=coefficients,
            rhs=rhs,
        )

as_greater_equal()

Return attributes in the greater equal convention

Returns:

Type Description
Tuple[List[str], List[float], float]

Tuple[List[str], List[float], float]: features, coefficients, rhs

Source code in bofire/data_models/constraints/linear.py
106
107
108
109
110
111
112
113
def as_greater_equal(self) -> Tuple[List[str], List[float], float]:
    """Return attributes in the greater equal convention

    Returns:
        Tuple[List[str], List[float], float]: features, coefficients, rhs

    """
    return self.features, [-1.0 * c for c in self.coefficients], -1.0 * self.rhs

as_smaller_equal()

Return attributes in the smaller equal convention

Returns:

Type Description
Tuple[List[str], List[float], float]

Tuple[List[str], List[float], float]: features, coefficients, rhs

Source code in bofire/data_models/constraints/linear.py
 97
 98
 99
100
101
102
103
104
def as_smaller_equal(self) -> Tuple[List[str], List[float], float]:
    """Return attributes in the smaller equal convention

    Returns:
        Tuple[List[str], List[float], float]: features, coefficients, rhs

    """
    return self.features, self.coefficients, self.rhs

from_greater_equal(features, coefficients, rhs) classmethod

Class method to construct linear inequality constraint of the form coefficients * x >= rhs.

Parameters:

Name Type Description Default
features List[str]

List of feature keys.

required
coefficients List[float]

List of coefficients.

required
rhs float

Right-hand side of the constraint.

required
Source code in bofire/data_models/constraints/linear.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
@classmethod
def from_greater_equal(
    cls,
    features: List[str],
    coefficients: List[float],
    rhs: float,
):
    """Class method to construct linear inequality constraint of the form `coefficients * x >= rhs`.

    Args:
        features (List[str]): List of feature keys.
        coefficients (List[float]): List of coefficients.
        rhs (float): Right-hand side of the constraint.

    """
    return cls(
        features=features,
        coefficients=[-1.0 * c for c in coefficients],
        rhs=-1.0 * rhs,
    )

from_smaller_equal(features, coefficients, rhs) classmethod

Class method to construct linear inequality constraint of the form coefficients * x <= rhs.

Parameters:

Name Type Description Default
features List[str]

List of feature keys.

required
coefficients List[float]

List of coefficients.

required
rhs float

Right-hand side of the constraint.

required
Source code in bofire/data_models/constraints/linear.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
@classmethod
def from_smaller_equal(
    cls,
    features: List[str],
    coefficients: List[float],
    rhs: float,
):
    """Class method to construct linear inequality constraint of the form `coefficients * x <= rhs`.

    Args:
        features (List[str]): List of feature keys.
        coefficients (List[float]): List of coefficients.
        rhs (float): Right-hand side of the constraint.

    """
    return cls(
        features=features,
        coefficients=coefficients,
        rhs=rhs,
    )

nchoosek

NChooseKConstraint

Bases: IntrapointConstraint

NChooseK constraint that defines how many ingredients are allowed in a formulation.

Attributes:

Name Type Description
features List[str]

List of feature keys to which the constraint applies.

min_count int

Minimal number of non-zero/active feature values.

max_count int

Maximum number of non-zero/active feature values.

none_also_valid bool

In case that min_count > 0, this flag decides if zero active features are also allowed.

Source code in bofire/data_models/constraints/nchoosek.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
class NChooseKConstraint(IntrapointConstraint):
    """NChooseK constraint that defines how many ingredients are allowed in a formulation.

    Attributes:
        features (List[str]): List of feature keys to which the constraint applies.
        min_count (int): Minimal number of non-zero/active feature values.
        max_count (int): Maximum number of non-zero/active feature values.
        none_also_valid (bool): In case that min_count > 0,
            this flag decides if zero active features are also allowed.

    """

    type: Literal["NChooseKConstraint"] = "NChooseKConstraint"
    min_count: int
    max_count: int
    none_also_valid: bool

    def validate_inputs(self, inputs: Inputs):
        keys = inputs.get_keys([ContinuousInput, DiscreteInput])
        for f in self.features:
            if f not in keys:
                raise ValueError(
                    f"Feature {f} is not a continuous input feature in the provided Inputs object.",
                )
            feature_ = inputs.get_by_key(f)
            assert isinstance(
                feature_, ContinuousInput
            ), f"Feature {f} is not a ContinuousInput."
            if feature_.bounds[0] < 0:
                raise ValueError(
                    f"Feature {f} must have a lower bound of >=0, but has {feature_.bounds[0]}",
                )

    @model_validator(mode="after")
    def validate_counts(self):
        """Validates if the minimum and maximum of allowed features are smaller than the overall number of features."""
        if self.min_count > len(self.features):
            raise ValueError("min_count must be <= # of features")
        if self.max_count > len(self.features):
            raise ValueError("max_count must be <= # of features")
        if self.min_count > self.max_count:
            raise ValueError("min_values must be <= max_values")

        return self

    def __call__(self, experiments: pd.DataFrame) -> pd.Series:
        """Smooth relaxation of NChooseK constraint by countig the number of zeros in a candidate by a sum of
        narrow gaussians centered at zero.

        Args:
            experiments (pd.DataFrame): Data to evaluate the constraint on.

        Returns:
            pd.Series containing the constraint violation for each experiment (row in experiments argument).

        """

        def relu(x):
            return np.maximum(0, x)

        indices = np.array(
            [i for i, name in enumerate(experiments.columns) if name in self.features],
            dtype=np.int64,
        )
        experiments_tensor = np.array(experiments.to_numpy())

        max_count_violation = np.zeros(experiments_tensor.shape[0])
        min_count_violation = np.zeros(experiments_tensor.shape[0])

        if self.max_count != len(self.features):
            max_count_violation = relu(
                -1 * narrow_gaussian(x=experiments_tensor[..., indices]).sum(axis=-1)
                + (len(self.features) - self.max_count),
            )

        if self.min_count > 0:
            min_count_violation = relu(
                narrow_gaussian(x=experiments_tensor[..., indices]).sum(axis=-1)
                - (len(self.features) - self.min_count),
            )

        return pd.Series(max_count_violation + min_count_violation)

    def is_fulfilled(self, experiments: pd.DataFrame, tol: float = 1e-6) -> pd.Series:
        """Check if the concurrency constraint is fulfilled for all the rows of the provided dataframe.

        Args:
            experiments (pd.DataFrame): Dataframe to evaluate constraint on.
            tol (float,optional): tolerance parameter. A constraint is considered as not fulfilled
                if the violation is larger than tol. Defaults to 1e-6.

        Returns:
            bool: True if fulfilled else False.

        """
        cols = self.features
        sums = (np.abs(experiments[cols]) > tol).sum(axis=1)

        lower = sums >= self.min_count
        upper = sums <= self.max_count

        if not self.none_also_valid:
            # return lower.all() and upper.all()
            return pd.Series(np.logical_and(lower, upper), index=experiments.index)
        none = sums == 0
        return pd.Series(
            np.logical_or(none, np.logical_and(lower, upper)),
            index=experiments.index,
        )

    def jacobian(self, experiments: pd.DataFrame) -> pd.DataFrame:
        raise NotImplementedError("Jacobian not implemented for NChooseK constraints.")

    def hessian(self, experiments: pd.DataFrame) -> Dict[Union[str, int], pd.DataFrame]:
        raise NotImplementedError("Hessian not implemented for NChooseK constraints.")

__call__(experiments)

Smooth relaxation of NChooseK constraint by countig the number of zeros in a candidate by a sum of narrow gaussians centered at zero.

Parameters:

Name Type Description Default
experiments DataFrame

Data to evaluate the constraint on.

required

Returns:

Type Description
Series

pd.Series containing the constraint violation for each experiment (row in experiments argument).

Source code in bofire/data_models/constraints/nchoosek.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
def __call__(self, experiments: pd.DataFrame) -> pd.Series:
    """Smooth relaxation of NChooseK constraint by countig the number of zeros in a candidate by a sum of
    narrow gaussians centered at zero.

    Args:
        experiments (pd.DataFrame): Data to evaluate the constraint on.

    Returns:
        pd.Series containing the constraint violation for each experiment (row in experiments argument).

    """

    def relu(x):
        return np.maximum(0, x)

    indices = np.array(
        [i for i, name in enumerate(experiments.columns) if name in self.features],
        dtype=np.int64,
    )
    experiments_tensor = np.array(experiments.to_numpy())

    max_count_violation = np.zeros(experiments_tensor.shape[0])
    min_count_violation = np.zeros(experiments_tensor.shape[0])

    if self.max_count != len(self.features):
        max_count_violation = relu(
            -1 * narrow_gaussian(x=experiments_tensor[..., indices]).sum(axis=-1)
            + (len(self.features) - self.max_count),
        )

    if self.min_count > 0:
        min_count_violation = relu(
            narrow_gaussian(x=experiments_tensor[..., indices]).sum(axis=-1)
            - (len(self.features) - self.min_count),
        )

    return pd.Series(max_count_violation + min_count_violation)

is_fulfilled(experiments, tol=1e-06)

Check if the concurrency constraint is fulfilled for all the rows of the provided dataframe.

Parameters:

Name Type Description Default
experiments DataFrame

Dataframe to evaluate constraint on.

required
tol (float, optional)

tolerance parameter. A constraint is considered as not fulfilled if the violation is larger than tol. Defaults to 1e-6.

1e-06

Returns:

Name Type Description
bool Series

True if fulfilled else False.

Source code in bofire/data_models/constraints/nchoosek.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
def is_fulfilled(self, experiments: pd.DataFrame, tol: float = 1e-6) -> pd.Series:
    """Check if the concurrency constraint is fulfilled for all the rows of the provided dataframe.

    Args:
        experiments (pd.DataFrame): Dataframe to evaluate constraint on.
        tol (float,optional): tolerance parameter. A constraint is considered as not fulfilled
            if the violation is larger than tol. Defaults to 1e-6.

    Returns:
        bool: True if fulfilled else False.

    """
    cols = self.features
    sums = (np.abs(experiments[cols]) > tol).sum(axis=1)

    lower = sums >= self.min_count
    upper = sums <= self.max_count

    if not self.none_also_valid:
        # return lower.all() and upper.all()
        return pd.Series(np.logical_and(lower, upper), index=experiments.index)
    none = sums == 0
    return pd.Series(
        np.logical_or(none, np.logical_and(lower, upper)),
        index=experiments.index,
    )

validate_counts()

Validates if the minimum and maximum of allowed features are smaller than the overall number of features.

Source code in bofire/data_models/constraints/nchoosek.py
49
50
51
52
53
54
55
56
57
58
59
@model_validator(mode="after")
def validate_counts(self):
    """Validates if the minimum and maximum of allowed features are smaller than the overall number of features."""
    if self.min_count > len(self.features):
        raise ValueError("min_count must be <= # of features")
    if self.max_count > len(self.features):
        raise ValueError("max_count must be <= # of features")
    if self.min_count > self.max_count:
        raise ValueError("min_values must be <= max_values")

    return self

nonlinear

NonlinearConstraint

Bases: IntrapointConstraint

Base class for nonlinear equality and inequality constraints.

Attributes:

Name Type Description
expression str

Mathematical expression that can be evaluated by pandas.eval.

jacobian_expression str

Mathematical expression that that can be evaluated by pandas.eval.

features list

list of feature keys (str) on which the constraint works on.

Source code in bofire/data_models/constraints/nonlinear.py
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
class NonlinearConstraint(IntrapointConstraint):
    """Base class for nonlinear equality and inequality constraints.

    Attributes:
        expression (str): Mathematical expression that can be evaluated by `pandas.eval`.
        jacobian_expression (str): Mathematical expression that that can be evaluated by `pandas.eval`.
        features (list): list of feature keys (str) on which the constraint works on.

    """

    expression: Union[str, Callable]
    jacobian_expression: Optional[Union[str, Callable]] = Field(
        default=None, validate_default=True
    )
    hessian_expression: Optional[Union[str, Callable]] = Field(
        default=None, validate_default=True
    )

    def validate_inputs(self, inputs: Inputs):
        keys = inputs.get_keys(ContinuousInput)
        for f in self.features:
            if f not in keys:
                raise ValueError(
                    f"Feature {f} is not a continuous input feature in the provided Inputs object.",
                )

    @model_validator(mode="after")
    def validate_features(self):
        if isinstance(self.expression, Callable):
            features = list(inspect.getfullargspec(self.expression).args)
            if set(features) != set(self.features):
                raise ValueError(
                    "Provided features do not match the features used in the expression.",
                )
        return self

    @field_validator("jacobian_expression")
    @classmethod
    def set_jacobian_expression(cls, jacobian_expression, info) -> Union[str, Callable]:
        if (
            jacobian_expression is None
            and "features" in info.data.keys()
            and "expression" in info.data.keys()
        ):
            try:
                import sympy
            except ImportError as e:
                warnings.warn(e.msg)
                warnings.warn("please run `pip install sympy` for this functionality.")
                return jacobian_expression
            if info.data["features"] is not None:
                if isinstance(info.data["expression"], str):
                    return (
                        "["
                        + ", ".join(
                            [
                                str(sympy.S(info.data["expression"]).diff(key))
                                for key in info.data["features"]
                            ],
                        )
                        + "]"
                    )

        return jacobian_expression

    @field_validator("hessian_expression")
    @classmethod
    def set_hessian_expression(cls, hessian_expression, info) -> Union[str, Callable]:
        if (
            hessian_expression is None
            and "features" in info.data.keys()
            and "expression" in info.data.keys()
        ):
            try:
                import sympy
            except ImportError as e:
                warnings.warn(e.msg)
                warnings.warn("please run `pip install sympy` for this functionality.")
                return hessian_expression
            if info.data["features"] is not None:
                if isinstance(info.data["expression"], str):
                    return (
                        "["
                        + ", ".join(
                            [
                                "["
                                + ", ".join(
                                    [
                                        str(
                                            sympy.S(info.data["expression"])
                                            .diff(key1)
                                            .diff(key2)
                                        )
                                        for key1 in info.data["features"]
                                    ]
                                )
                                + "]"
                                for key2 in info.data["features"]
                            ]
                        )
                        + "]"
                    )

        return hessian_expression

    def __call__(self, experiments: pd.DataFrame) -> pd.Series:
        if isinstance(self.expression, str):
            return experiments.eval(self.expression)
        elif isinstance(self.expression, Callable):
            func_input = {
                col: torch_tensor(experiments[col], requires_grad=False)
                for col in experiments.columns
            }
            return pd.Series(self.expression(**func_input).cpu().numpy())

    def jacobian(self, experiments: pd.DataFrame) -> pd.DataFrame:
        if self.jacobian_expression is not None:
            if isinstance(self.jacobian_expression, str):
                res = experiments.eval(self.jacobian_expression)
                for i, col in enumerate(res):
                    if not hasattr(col, "__iter__"):
                        res[i] = pd.Series(np.repeat(col, experiments.shape[0]))

                if self.features is not None:
                    return pd.DataFrame(
                        res,
                        index=["dg/d" + name for name in self.features],
                    ).transpose()
                return pd.DataFrame(
                    res,
                    index=[f"dg/dx{i}" for i in range(experiments.shape[1])],
                ).transpose()
            elif isinstance(self.jacobian_expression, Callable):
                args = inspect.getfullargspec(self.jacobian_expression).args

                func_input = {
                    arg: torch_tensor(experiments[arg], requires_grad=False)
                    for arg in args
                }
                result = self.jacobian_expression(**func_input)

                return pd.DataFrame(
                    np.array(
                        [
                            result[args.index(col)]
                            if col in args
                            else np.zeros(shape=(len(experiments)))
                            for col in experiments.columns
                        ]
                    ),
                    index=["dg/d" + name for name in experiments.columns],
                ).transpose()
        elif isinstance(self.expression, Callable):
            args = inspect.getfullargspec(self.expression).args

            func_input = tuple(
                [torch_tensor(experiments[arg], requires_grad=False) for arg in args]
            )

            result = torch_jacobian(self.expression, func_input)
            result = [torch_diag(result[i]).cpu().numpy() for i in range(len(args))]

            return pd.DataFrame(
                np.array([result[args.index(col)] for col in args]),
                index=["dg/d" + name for name in args],
            ).transpose()

        raise ValueError(
            "The jacobian of a nonlinear constraint cannot be evaluated if jacobian_expression is None and expression is not Callable.",
        )

    def hessian(self, experiments: pd.DataFrame) -> Dict[Union[str, int], pd.DataFrame]:
        """
        Computes a dict of dataframes where the key dimension is the index of the experiments dataframe
        and the value is the hessian matrix of the constraint evaluated at the corresponding experiment.

        Args:
            experiments (pd.DataFrame): Dataframe to evaluate the constraint Hessian on.

        Returns:
            Dict[pd.DataFrame]: Dictionary of dataframes where the key is the index of the experiments dataframe
            and the value is the Hessian matrix of the constraint evaluated at the corresponding experiment.
        """
        if self.hessian_expression is not None:
            if isinstance(self.hessian_expression, str):
                res = experiments.eval(self.hessian_expression)
            else:
                if not isinstance(self.hessian_expression, Callable):
                    raise ValueError(
                        "The hessian_expression must be a string or a callable.",
                    )
                args = inspect.getfullargspec(self.hessian_expression).args

                func_input = {
                    arg: torch_tensor(experiments[arg], requires_grad=False)
                    for arg in args
                }
                res = self.hessian_expression(**func_input)
            for i, _ in enumerate(res):
                for j, entry in enumerate(res[i]):
                    if not hasattr(entry, "__iter__"):
                        res[i][j] = pd.Series(np.repeat(entry, experiments.shape[0]))
            res = np.array(res)
            names = self.features or [f"x{i}" for i in range(experiments.shape[1])]

            return {
                idx: pd.DataFrame(
                    res[..., i],
                    columns=[f"d/d{name}" for name in names],
                    index=[f"d/d{name}" for name in names],
                )
                for i, idx in enumerate(experiments.index)
            }

        elif isinstance(self.expression, Callable):
            args = inspect.getfullargspec(self.expression).args

            func_input = {
                idx: tuple(
                    [
                        torch_tensor(experiments[arg][idx], requires_grad=False)
                        for arg in args
                    ]
                )
                for idx in experiments.index
            }

            names = self.features or [f"x{i}" for i in range(experiments.shape[1])]
            res = {
                idx: pd.DataFrame(
                    np.array(torch_hessian(self.expression, func_input[idx])),
                    columns=[f"d/d{name}" for name in names],
                    index=[f"d/d{name}" for name in names],
                )
                for idx in experiments.index
            }
            return res

        raise ValueError(
            "The hessian of a nonlinear constraint cannot be evaluated if hessian_expression is None and expression is not Callable.",
        )

hessian(experiments)

Computes a dict of dataframes where the key dimension is the index of the experiments dataframe and the value is the hessian matrix of the constraint evaluated at the corresponding experiment.

Parameters:

Name Type Description Default
experiments DataFrame

Dataframe to evaluate the constraint Hessian on.

required

Returns:

Type Description
Dict[Union[str, int], DataFrame]

Dict[pd.DataFrame]: Dictionary of dataframes where the key is the index of the experiments dataframe

Dict[Union[str, int], DataFrame]

and the value is the Hessian matrix of the constraint evaluated at the corresponding experiment.

Source code in bofire/data_models/constraints/nonlinear.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
def hessian(self, experiments: pd.DataFrame) -> Dict[Union[str, int], pd.DataFrame]:
    """
    Computes a dict of dataframes where the key dimension is the index of the experiments dataframe
    and the value is the hessian matrix of the constraint evaluated at the corresponding experiment.

    Args:
        experiments (pd.DataFrame): Dataframe to evaluate the constraint Hessian on.

    Returns:
        Dict[pd.DataFrame]: Dictionary of dataframes where the key is the index of the experiments dataframe
        and the value is the Hessian matrix of the constraint evaluated at the corresponding experiment.
    """
    if self.hessian_expression is not None:
        if isinstance(self.hessian_expression, str):
            res = experiments.eval(self.hessian_expression)
        else:
            if not isinstance(self.hessian_expression, Callable):
                raise ValueError(
                    "The hessian_expression must be a string or a callable.",
                )
            args = inspect.getfullargspec(self.hessian_expression).args

            func_input = {
                arg: torch_tensor(experiments[arg], requires_grad=False)
                for arg in args
            }
            res = self.hessian_expression(**func_input)
        for i, _ in enumerate(res):
            for j, entry in enumerate(res[i]):
                if not hasattr(entry, "__iter__"):
                    res[i][j] = pd.Series(np.repeat(entry, experiments.shape[0]))
        res = np.array(res)
        names = self.features or [f"x{i}" for i in range(experiments.shape[1])]

        return {
            idx: pd.DataFrame(
                res[..., i],
                columns=[f"d/d{name}" for name in names],
                index=[f"d/d{name}" for name in names],
            )
            for i, idx in enumerate(experiments.index)
        }

    elif isinstance(self.expression, Callable):
        args = inspect.getfullargspec(self.expression).args

        func_input = {
            idx: tuple(
                [
                    torch_tensor(experiments[arg][idx], requires_grad=False)
                    for arg in args
                ]
            )
            for idx in experiments.index
        }

        names = self.features or [f"x{i}" for i in range(experiments.shape[1])]
        res = {
            idx: pd.DataFrame(
                np.array(torch_hessian(self.expression, func_input[idx])),
                columns=[f"d/d{name}" for name in names],
                index=[f"d/d{name}" for name in names],
            )
            for idx in experiments.index
        }
        return res

    raise ValueError(
        "The hessian of a nonlinear constraint cannot be evaluated if hessian_expression is None and expression is not Callable.",
    )

NonlinearEqualityConstraint

Bases: NonlinearConstraint, EqualityConstraint

Nonlinear equality constraint of the form 'expression == 0'.

Attributes:

Name Type Description
expression Union[str, Callable]

Mathematical expression that can be evaluated by pandas.eval.

Source code in bofire/data_models/constraints/nonlinear.py
279
280
281
282
283
284
285
286
287
class NonlinearEqualityConstraint(NonlinearConstraint, EqualityConstraint):
    """Nonlinear equality constraint of the form 'expression == 0'.

    Attributes:
        expression: Mathematical expression that can be evaluated by `pandas.eval`.

    """

    type: Literal["NonlinearEqualityConstraint"] = "NonlinearEqualityConstraint"

NonlinearInequalityConstraint

Bases: NonlinearConstraint, InequalityConstraint

Nonlinear inequality constraint of the form 'expression <= 0'.

Attributes:

Name Type Description
expression Union[str, Callable]

Mathematical expression that can be evaluated by pandas.eval.

Source code in bofire/data_models/constraints/nonlinear.py
290
291
292
293
294
295
296
297
298
class NonlinearInequalityConstraint(NonlinearConstraint, InequalityConstraint):
    """Nonlinear inequality constraint of the form 'expression <= 0'.

    Attributes:
        expression: Mathematical expression that can be evaluated by `pandas.eval`.

    """

    type: Literal["NonlinearInequalityConstraint"] = "NonlinearInequalityConstraint"

product

ProductConstraint

Bases: IntrapointConstraint

Represents a product constraint of the form sign * x1**e1 * x2**e2 * ... * xn**en.

Attributes:

Name Type Description
type str

The type of the constraint.

features FeatureKeys

The keys of the features used in the constraint.

exponents List[float]

The exponents corresponding to each feature.

rhs float

The right-hand side value of the constraint.

sign Literal[1, -1]

The sign of the left hand side of the constraint. Defaults to 1.

Source code in bofire/data_models/constraints/product.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
class ProductConstraint(IntrapointConstraint):
    """Represents a product constraint of the form `sign * x1**e1 * x2**e2 * ... * xn**en`.

    Attributes:
        type (str): The type of the constraint.
        features (FeatureKeys): The keys of the features used in the constraint.
        exponents (List[float]): The exponents corresponding to each feature.
        rhs (float): The right-hand side value of the constraint.
        sign (Literal[1, -1], optional): The sign of the left hand side of the constraint.
            Defaults to 1.

    """

    type: str
    exponents: Annotated[List[float], Field(min_length=2)]
    rhs: float
    sign: Literal[1, -1] = 1

    @model_validator(mode="after")
    def validate_list_lengths(self) -> "ProductConstraint":
        """Validates that the number of features and exponents provided are the same.

        Raises:
            ValueError: If the number of features and exponents are not equal.

        Returns:
            ProductConstraint: The current instance of the class.

        """
        if len(self.features) != len(self.exponents):
            raise ValueError(
                f"must provide same number of features and exponents, got {len(self.features)} != {len(self.exponents)}",
            )
        return self

    def validate_inputs(self, inputs: Inputs):
        keys = inputs.get_keys(ContinuousInput)
        for f in self.features:
            if f not in keys:
                raise ValueError(
                    f"Feature {f} is not a continuous input feature in the provided Inputs object.",
                )

    def __call__(self, experiments: pd.DataFrame) -> pd.Series:
        """Evaluates the constraint on the given experiments.

        Args:
            experiments (pd.DataFrame): The experiments to evaluate the constraint on.

        Returns:
            pd.Series: The distance to reach constraint fulfillment.

        """
        return pd.Series(
            self.sign
            * np.prod(
                np.power(experiments[self.features].values, np.array(self.exponents)),
                axis=1,
            )
            - self.rhs,
            index=experiments.index,
        )

    def jacobian(self, experiments: pd.DataFrame) -> pd.DataFrame:
        raise NotImplementedError(
            "Jacobian for product constraints is not yet implemented.",
        )

    def hessian(self, experiments: pd.DataFrame) -> List[pd.DataFrame]:
        raise NotImplementedError(
            "Hessian for product constraints is not yet implemented.",
        )

__call__(experiments)

Evaluates the constraint on the given experiments.

Parameters:

Name Type Description Default
experiments DataFrame

The experiments to evaluate the constraint on.

required

Returns:

Type Description
Series

pd.Series: The distance to reach constraint fulfillment.

Source code in bofire/data_models/constraints/product.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def __call__(self, experiments: pd.DataFrame) -> pd.Series:
    """Evaluates the constraint on the given experiments.

    Args:
        experiments (pd.DataFrame): The experiments to evaluate the constraint on.

    Returns:
        pd.Series: The distance to reach constraint fulfillment.

    """
    return pd.Series(
        self.sign
        * np.prod(
            np.power(experiments[self.features].values, np.array(self.exponents)),
            axis=1,
        )
        - self.rhs,
        index=experiments.index,
    )

validate_list_lengths()

Validates that the number of features and exponents provided are the same.

Raises:

Type Description
ValueError

If the number of features and exponents are not equal.

Returns:

Name Type Description
ProductConstraint ProductConstraint

The current instance of the class.

Source code in bofire/data_models/constraints/product.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@model_validator(mode="after")
def validate_list_lengths(self) -> "ProductConstraint":
    """Validates that the number of features and exponents provided are the same.

    Raises:
        ValueError: If the number of features and exponents are not equal.

    Returns:
        ProductConstraint: The current instance of the class.

    """
    if len(self.features) != len(self.exponents):
        raise ValueError(
            f"must provide same number of features and exponents, got {len(self.features)} != {len(self.exponents)}",
        )
    return self

ProductEqualityConstraint

Bases: ProductConstraint, EqualityConstraint

Represents a product constraint of the form sign * x1**e1 * x2**e2 * ... * xn**en == rhs.

Attributes:

Name Type Description
type str

The type of the constraint.

features FeatureKeys

The keys of the features used in the constraint.

exponents List[float]

The exponents corresponding to each feature.

rhs float

The right-hand side value of the constraint.

sign Literal[1, -1]

The sign of the left hand side of the constraint. Defaults to 1.

Source code in bofire/data_models/constraints/product.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
class ProductEqualityConstraint(ProductConstraint, EqualityConstraint):
    """Represents a product constraint of the form `sign * x1**e1 * x2**e2 * ... * xn**en == rhs`.

    Attributes:
        type (str): The type of the constraint.
        features (FeatureKeys): The keys of the features used in the constraint.
        exponents (List[float]): The exponents corresponding to each feature.
        rhs (float): The right-hand side value of the constraint.
        sign (Literal[1, -1], optional): The sign of the left hand side of the constraint.
            Defaults to 1.

    """

    type: Literal["ProductEqualityConstraint"] = "ProductEqualityConstraint"

ProductInequalityConstraint

Bases: ProductConstraint, InequalityConstraint

Represents a product constraint of the form sign * x1**e1 * x2**e2 * ... * xn**en <= rhs.

Attributes:

Name Type Description
type str

The type of the constraint.

features FeatureKeys

The keys of the features used in the constraint.

exponents List[float]

The exponents corresponding to each feature.

rhs float

The right-hand side value of the constraint.

sign Literal[1, -1]

The sign of the left hand side of the constraint. Defaults to 1.

Source code in bofire/data_models/constraints/product.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
class ProductInequalityConstraint(ProductConstraint, InequalityConstraint):
    """Represents a product constraint of the form `sign * x1**e1 * x2**e2 * ... * xn**en <= rhs`.

    Attributes:
        type (str): The type of the constraint.
        features (FeatureKeys): The keys of the features used in the constraint.
        exponents (List[float]): The exponents corresponding to each feature.
        rhs (float): The right-hand side value of the constraint.
        sign (Literal[1, -1], optional): The sign of the left hand side of the constraint.
            Defaults to 1.

    """

    type: Literal["ProductInequalityConstraint"] = "ProductInequalityConstraint"