Skip to content

Commit

Permalink
Universal constraint sampler (#328)
Browse files Browse the repository at this point in the history
  • Loading branch information
Osburg authored Jan 10, 2024
1 parent 591401c commit 33a2053
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 1 deletion.
6 changes: 5 additions & 1 deletion bofire/data_models/strategies/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
from bofire.data_models.strategies.samplers.polytope import PolytopeSampler
from bofire.data_models.strategies.samplers.rejection import RejectionSampler
from bofire.data_models.strategies.samplers.sampler import SamplerStrategy
from bofire.data_models.strategies.samplers.universal_constraint import (
UniversalConstraintSampler,
)
from bofire.data_models.strategies.stepwise.conditions import ( # noqa: F401
AlwaysTrueCondition,
CombiCondition,
Expand Down Expand Up @@ -50,6 +53,7 @@
QparegoStrategy,
PolytopeSampler,
RejectionSampler,
UniversalConstraintSampler,
RandomStrategy,
DoEStrategy,
StepwiseStrategy,
Expand All @@ -68,7 +72,7 @@
MoboStrategy,
]

AnySampler = Union[PolytopeSampler, RejectionSampler]
AnySampler = Union[PolytopeSampler, RejectionSampler, UniversalConstraintSampler]


AnyCondition = Union[NumberOfExperimentsCondition, CombiCondition, AlwaysTrueCondition]
49 changes: 49 additions & 0 deletions bofire/data_models/strategies/samplers/universal_constraint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from typing import Annotated, Literal, Type

from pydantic import Field

from bofire.data_models.constraints.api import (
LinearEqualityConstraint,
LinearInequalityConstraint,
NChooseKConstraint,
NonlinearEqualityConstraint,
NonlinearInequalityConstraint,
)
from bofire.data_models.features.api import (
ContinuousInput,
ContinuousOutput,
Feature,
)
from bofire.data_models.strategies.strategy import Strategy


class UniversalConstraintSampler(Strategy):
"""Sampler that generates samples by optimization in IPOPT.
Attributes:
domain (Domain): Domain defining the constrained input space
sampling_fraction (float, optional): Fraction of sampled points to total points generated in
the sampling process. Defaults to 0.3.
ipopt_options (dict, optional): Dictionary containing options for the IPOPT solver. Defaults to {"maxiter":200, "disp"=0}.
"""

type: Literal["UniversalConstraintSampler"] = "UniversalConstraintSampler"
sampling_fraction: Annotated[float, Field(gt=0, lt=1)] = 0.3
ipopt_options: dict = {"maxiter": 200, "disp": 0}

@classmethod
def is_constraint_implemented(cls, my_type: Type[Feature]) -> bool:
return my_type in [
LinearEqualityConstraint,
LinearInequalityConstraint,
NonlinearInequalityConstraint,
NonlinearEqualityConstraint,
NChooseKConstraint,
]

@classmethod
def is_feature_implemented(cls, my_type: Type[Feature]) -> bool:
return my_type in [
ContinuousInput,
ContinuousOutput,
]
3 changes: 3 additions & 0 deletions bofire/strategies/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@
from bofire.strategies.samplers.polytope import PolytopeSampler # noqa: F401
from bofire.strategies.samplers.rejection import RejectionSampler # noqa: F401
from bofire.strategies.samplers.sampler import SamplerStrategy # noqa: F401
from bofire.strategies.samplers.universal_constraint import ( # noqa: F401
UniversalConstraintSampler,
)
from bofire.strategies.stepwise.stepwise import StepwiseStrategy # noqa: F401
from bofire.strategies.strategy import Strategy # noqa: F401
4 changes: 4 additions & 0 deletions bofire/strategies/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
from bofire.strategies.samplers.polytope import PolytopeSampler # noqa: F401
from bofire.strategies.samplers.rejection import RejectionSampler # noqa: F401
from bofire.strategies.samplers.sampler import SamplerStrategy # noqa: F401
from bofire.strategies.samplers.universal_constraint import ( # noqa: F401
UniversalConstraintSampler,
)
from bofire.strategies.stepwise.stepwise import StepwiseStrategy
from bofire.strategies.strategy import Strategy # noqa: F401

Expand All @@ -33,6 +36,7 @@
data_models.QparegoStrategy: QparegoStrategy,
data_models.PolytopeSampler: PolytopeSampler,
data_models.RejectionSampler: RejectionSampler,
data_models.UniversalConstraintSampler: UniversalConstraintSampler,
data_models.DoEStrategy: DoEStrategy,
data_models.StepwiseStrategy: StepwiseStrategy,
data_models.FactorialStrategy: FactorialStrategy,
Expand Down
55 changes: 55 additions & 0 deletions bofire/strategies/samplers/universal_constraint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import pandas as pd

from bofire.data_models.strategies.api import UniversalConstraintSampler as DataModel
from bofire.strategies.doe.design import find_local_max_ipopt
from bofire.strategies.enum import OptimalityCriterionEnum
from bofire.strategies.strategy import Strategy


class UniversalConstraintSampler(Strategy):
"""Sampler that generates samples by optimization in IPOPT.
Attributes:
domain (Domain): Domain defining the constrained input space
sampling_fraction (float, optional): Fraction of sampled points to total points generated in
the sampling process. Defaults to 0.3.
ipopt_options (dict, optional): Dictionary containing options for the IPOPT solver. Defaults to {"maxiter":200, "disp"=0}.
"""

def __init__(
self,
data_model: DataModel,
**kwargs,
):
super().__init__(data_model=data_model, **kwargs)
assert data_model.sampling_fraction > 0 and data_model.sampling_fraction <= 1
self.sampling_fraction = data_model.sampling_fraction
self.ipopt_options = data_model.ipopt_options

def _ask(self, candidate_count: int) -> pd.DataFrame:
samples = find_local_max_ipopt(
domain=self.domain,
model_type="linear", # dummy model
n_experiments=self.num_candidates
+ int(candidate_count / self.sampling_fraction),
ipopt_options=self.ipopt_options,
objective=OptimalityCriterionEnum.SPACE_FILLING,
fixed_experiments=self.candidates,
)

samples = samples.iloc[
self.num_candidates :,
]
samples = samples.sample(
n=candidate_count,
replace=False,
ignore_index=True,
random_state=self._get_seed(),
)

self.domain.validate_experiments(samples)

return samples

def has_sufficient_experiments(self) -> bool:
return True
9 changes: 9 additions & 0 deletions tests/bofire/data_models/specs/strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,15 @@
"seed": 42,
},
)
specs.add_valid(
strategies.UniversalConstraintSampler,
lambda: {
"domain": domain.valid().obj().dict(),
"sampling_fraction": 0.3,
"ipopt_options": {"maxiter": 200, "disp": 0},
"seed": 42,
},
)

tempdomain = domain.valid().obj().dict()

Expand Down
53 changes: 53 additions & 0 deletions tests/bofire/strategies/test_samplers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
from pandas import concat

import bofire.data_models.strategies.api as data_models
import bofire.strategies.api as strategies
Expand All @@ -7,10 +8,62 @@
LinearEqualityConstraint,
LinearInequalityConstraint,
NChooseKConstraint,
NonlinearEqualityConstraint,
NonlinearInequalityConstraint,
)
from bofire.data_models.domain.api import Constraints, Domain, Inputs
from bofire.data_models.features.api import CategoricalInput, ContinuousInput

inputs = [ContinuousInput(key=f"if{i}", bounds=(0, 1)) for i in range(1, 4)]
c1 = LinearInequalityConstraint(
features=["if1", "if2", "if3"], coefficients=[1, 1, 1], rhs=1
)
c2 = LinearEqualityConstraint(
features=["if1", "if2", "if3"], coefficients=[1, 1, 1], rhs=1
)
c3 = NonlinearEqualityConstraint(
expression="if1**2 + if2**2 - if3", features=["if1", "if2", "if3"]
)
c4 = NonlinearInequalityConstraint(
expression="if1**2 + if2**2 - if3", features=["if1", "if2", "if3"]
)
c5 = NChooseKConstraint(
features=["if1", "if2", "if3"], min_count=0, max_count=1, none_also_valid=True
)


domains = [
Domain.from_lists(inputs=inputs, constraints=[c1]),
Domain.from_lists(inputs=inputs, constraints=[c2]),
Domain.from_lists(inputs=inputs, constraints=[c3]),
Domain.from_lists(inputs=inputs, constraints=[c4]),
Domain.from_lists(inputs=inputs, constraints=[c5]),
]


@pytest.mark.parametrize(
"domain, num_samples",
[(domain, candidate_count) for domain in domains for candidate_count in [1, 16]],
)
def test_UniversalConstraintSampler(domain, num_samples):
data_model = data_models.UniversalConstraintSampler(domain=domain)
sampler = strategies.UniversalConstraintSampler(data_model=data_model)
samples = sampler.ask(num_samples)
assert len(samples) == num_samples


def test_UniversalConstraintSampler_pending_candidates():
data_model = data_models.UniversalConstraintSampler(domain=domains[0])
sampler = strategies.UniversalConstraintSampler(data_model=data_model)
pending_candidates = sampler.ask(2, add_pending=True)
samples = sampler.ask(1)
assert len(samples) == 1
all_samples = concat(
[samples, pending_candidates], axis=0, ignore_index=True
).drop_duplicates()
assert len(all_samples) == 3


inputs = Inputs(
features=[ContinuousInput(key=f"if{i}", bounds=(0, 1)) for i in range(1, 4)]
)
Expand Down

0 comments on commit 33a2053

Please sign in to comment.