Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multilinear constraint #348

Merged
merged 9 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading