Skip to content

Commit

Permalink
Product constraint (experimental-design#348)
Browse files Browse the repository at this point in the history
  • Loading branch information
jduerholt authored Feb 7, 2024
1 parent bf2a119 commit 67f3320
Show file tree
Hide file tree
Showing 29 changed files with 535 additions and 281 deletions.
14 changes: 12 additions & 2 deletions bofire/data_models/constraints/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
Constraint,
ConstraintError,
ConstraintNotFulfilledError,
EqalityConstraint,
InequalityConstraint,
IntrapointConstraint,
)
from bofire.data_models.constraints.interpoint import (
Expand All @@ -21,13 +23,21 @@
NonlinearEqualityConstraint,
NonlinearInequalityConstraint,
)
from bofire.data_models.constraints.product import (
ProductConstraint,
ProductEqualityConstraint,
ProductInequalityConstraint,
)

AbstractConstraint = Union[
Constraint,
LinearConstraint,
NonlinearConstraint,
IntrapointConstraint,
InterpointConstraint,
ProductConstraint,
InequalityConstraint,
EqalityConstraint,
]

AnyConstraint = Union[
Expand All @@ -37,8 +47,8 @@
NonlinearInequalityConstraint,
NChooseKConstraint,
InterpointEqualityConstraint,
ProductEqualityConstraint,
ProductInequalityConstraint,
]

AnyConstraintError = Union[ConstraintError, ConstraintNotFulfilledError]

# %%
25 changes: 18 additions & 7 deletions bofire/data_models/constraints/constraint.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from abc import abstractmethod
from typing import List, Optional
from typing import Optional

import numpy as np
import pandas as pd
from pydantic import Field
from typing_extensions import Annotated

from bofire.data_models.base import BaseModel

Expand Down Expand Up @@ -60,6 +59,22 @@ class IntrapointConstraint(Constraint):
type: str


class EqalityConstraint(IntrapointConstraint):
type: str

def is_fulfilled(self, experiments: pd.DataFrame, tol: float = 1e-6) -> pd.Series:
return pd.Series(
np.isclose(self(experiments), 0, atol=tol), index=experiments.index
)


class InequalityConstraint(IntrapointConstraint):
type: str

def is_fulfilled(self, experiments: pd.DataFrame, tol: float = 1e-6) -> pd.Series:
return self(experiments) <= 0 + tol


class ConstraintError(Exception):
"""Base Error for Constraints"""

Expand All @@ -70,7 +85,3 @@ class ConstraintNotFulfilledError(ConstraintError):
"""Raised when an constraint is not fulfilled."""

pass


FeatureKeys = Annotated[List[str], Field(min_length=2)]
Coefficients = Annotated[List[float], Field(min_length=2)]
81 changes: 14 additions & 67 deletions bofire/data_models/constraints/linear.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from typing import List, Literal, Tuple
from typing import Annotated, List, Literal, Tuple

import numpy as np
import pandas as pd
from pydantic import field_validator, model_validator
from pydantic import Field, model_validator

from bofire.data_models.constraints.constraint import (
Coefficients,
FeatureKeys,
EqalityConstraint,
InequalityConstraint,
IntrapointConstraint,
)
from bofire.data_models.types import TFeatureKeys


class LinearConstraint(IntrapointConstraint):
Expand All @@ -22,18 +23,10 @@ class LinearConstraint(IntrapointConstraint):

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

features: FeatureKeys
coefficients: Coefficients
features: TFeatureKeys
coefficients: Annotated[List[float], Field(min_length=2)]
rhs: float

@field_validator("features")
@classmethod
def validate_features_unique(cls, features):
"""Validate that feature keys are unique."""
if len(features) != len(set(features)):
raise ValueError("features must be unique")
return features

@model_validator(mode="after")
def validate_list_lengths(self):
"""Validate that length of the feature and coefficient lists have the same length."""
Expand All @@ -46,29 +39,22 @@ def validate_list_lengths(self):
def __call__(self, experiments: pd.DataFrame) -> pd.Series:
return (
experiments[self.features] @ self.coefficients - self.rhs
) / np.linalg.norm(self.coefficients)

def __str__(self) -> str:
"""Generate string representation of the constraint.
Returns:
str: string representation of the constraint.
"""
return " + ".join(
[f"{self.coefficients[i]} * {feat}" for i, feat in enumerate(self.features)]
)
) / 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(self.coefficients)],
[
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],
)


class LinearEqualityConstraint(LinearConstraint):
class LinearEqualityConstraint(LinearConstraint, EqalityConstraint):
"""Linear equality constraint of the form `coefficients * x = rhs`.
Attributes:
Expand All @@ -79,36 +65,8 @@ class LinearEqualityConstraint(LinearConstraint):

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

# def is_fulfilled(self, experiments: pd.DataFrame, complete: bool) -> bool:
# """Check if the linear equality constraint is fulfilled for all the rows of the provided dataframe.

# Args:
# df_data (pd.DataFrame): Dataframe to evaluate constraint on.

# Returns:
# bool: True if fulfilled else False.
# """
# fulfilled = np.isclose(self(experiments), 0)
# if complete:
# return fulfilled.all()
# else:
# pd.Series(fulfilled, index=experiments.index)

def is_fulfilled(self, experiments: pd.DataFrame, tol: float = 1e-6) -> pd.Series:
return pd.Series(
np.isclose(self(experiments), 0, atol=tol), index=experiments.index
)

def __str__(self) -> str:
"""Generate string representation of the constraint.
Returns:
str: string representation of the constraint.
"""
return super().__str__() + f" = {self.rhs}"


class LinearInequalityConstraint(LinearConstraint):
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
Expand All @@ -122,9 +80,6 @@ class LinearInequalityConstraint(LinearConstraint):

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

def is_fulfilled(self, experiments: pd.DataFrame, tol: float = 1e-6) -> pd.Series:
return self(experiments) <= 0 + tol

def as_smaller_equal(self) -> Tuple[List[str], List[float], float]:
"""Return attributes in the smaller equal convention
Expand Down Expand Up @@ -180,11 +135,3 @@ def from_smaller_equal(
coefficients=coefficients,
rhs=rhs,
)

def __str__(self):
"""Generate string representation of the constraint.
Returns:
str: string representation of the constraint.
"""
return super().__str__() + f" <= {self.rhs}"
20 changes: 3 additions & 17 deletions bofire/data_models/constraints/nchoosek.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import pandas as pd
from pydantic import field_validator, model_validator

from bofire.data_models.constraints.constraint import FeatureKeys, IntrapointConstraint
from bofire.data_models.constraints.constraint import IntrapointConstraint
from bofire.data_models.types import TFeatureKeys


def narrow_gaussian(x, ell=1e-3):
Expand All @@ -23,7 +24,7 @@ class NChooseKConstraint(IntrapointConstraint):
"""

type: Literal["NChooseKConstraint"] = "NChooseKConstraint"
features: FeatureKeys
features: TFeatureKeys
min_count: int
max_count: int
none_also_valid: bool
Expand Down Expand Up @@ -114,20 +115,5 @@ def is_fulfilled(self, experiments: pd.DataFrame, tol: float = 1e-6) -> pd.Serie
index=experiments.index,
)

def __str__(self):
"""Generate string representation of the constraint.
Returns:
str: string representation of the constraint.
"""
res = (
"of the features "
+ ", ".join(self.features)
+ f" between {self.min_count} and {self.max_count} must be used"
)
if self.none_also_valid:
res += " (none is also ok)"
return res

def jacobian(self, experiments: pd.DataFrame) -> pd.DataFrame:
raise NotImplementedError("Jacobian not implemented for NChooseK constraints.")
27 changes: 9 additions & 18 deletions bofire/data_models/constraints/nonlinear.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
import pandas as pd
from pydantic import Field, field_validator

from bofire.data_models.constraints.constraint import FeatureKeys, IntrapointConstraint
from bofire.data_models.constraints.constraint import (
EqalityConstraint,
InequalityConstraint,
IntrapointConstraint,
)
from bofire.data_models.types import TFeatureKeys


class NonlinearConstraint(IntrapointConstraint):
Expand All @@ -18,7 +23,7 @@ class NonlinearConstraint(IntrapointConstraint):
"""

expression: str
features: Optional[FeatureKeys] = None
features: Optional[TFeatureKeys] = None
jacobian_expression: Optional[str] = Field(default=None, validate_default=True)

@field_validator("jacobian_expression")
Expand Down Expand Up @@ -73,7 +78,7 @@ def jacobian(self, experiments: pd.DataFrame) -> pd.DataFrame:
)


class NonlinearEqualityConstraint(NonlinearConstraint):
class NonlinearEqualityConstraint(NonlinearConstraint, EqalityConstraint):
"""Nonlinear equality constraint of the form 'expression == 0'.
Attributes:
Expand All @@ -82,26 +87,12 @@ class NonlinearEqualityConstraint(NonlinearConstraint):

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

def is_fulfilled(self, experiments: pd.DataFrame, tol: float = 1e-6) -> pd.Series:
return pd.Series(
np.isclose(self(experiments), 0, atol=tol), index=experiments.index
)

def __str__(self):
return f"{self.expression}==0"


class NonlinearInequalityConstraint(NonlinearConstraint):
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"

def is_fulfilled(self, experiments: pd.DataFrame, tol: float = 1e-6) -> pd.Series:
return self(experiments) <= 0 + tol

def __str__(self):
return f"{self.expression}<=0"
Loading

0 comments on commit 67f3320

Please sign in to comment.