Skip to content

Commit

Permalink
Transforms (#355)
Browse files Browse the repository at this point in the history
* less max_parallelism

* fixed test parametrization

* removed max_parallel-test

* added valid/invalid serializations to tests

* register invalid

* separate transform data model

* ignore vscode's devcontainer

* separate api file

* added init and TF constructor

* rename to `DropDataTransform`

* Apply suggestions from code review

Co-authored-by: Johannes P. Dürholt <[email protected]>

* Apply suggestions from code review

Co-authored-by: Johannes P. Dürholt <[email protected]>

* docstring, register valid specs, rename to untransform_candidates

* move test to separate file and extend

---------

Co-authored-by: Johannes P. Dürholt <[email protected]>
  • Loading branch information
bertiqwerty and jduerholt authored Mar 1, 2024
1 parent ccb0bc2 commit 7909654
Show file tree
Hide file tree
Showing 21 changed files with 294 additions and 163 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ venv.bak/

# vscode project settings
.vscode/
.devcontainer

# Rope project settings
.ropeproject
Expand All @@ -133,3 +134,4 @@ dmypy.json

# generated version file
bofire/version.py

8 changes: 5 additions & 3 deletions bofire/data_models/strategies/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from bofire.data_models.strategies.space_filling import SpaceFillingStrategy
from bofire.data_models.strategies.stepwise.conditions import ( # noqa: F401
AlwaysTrueCondition,
AnyCondition,
CombiCondition,
NumberOfExperimentsCondition,
)
Expand All @@ -30,6 +31,10 @@
StepwiseStrategy,
)
from bofire.data_models.strategies.strategy import Strategy
from bofire.data_models.transforms.api import ( # noqa: F401
AnyTransform,
DropDataTransform,
)

AbstractStrategy = Union[
Strategy,
Expand Down Expand Up @@ -67,7 +72,4 @@
]


AnyCondition = Union[NumberOfExperimentsCondition, CombiCondition, AlwaysTrueCondition]


AnyLocalSearchConfig = LSRBO
41 changes: 37 additions & 4 deletions bofire/data_models/strategies/stepwise/conditions.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
from typing import List, Literal, Union
from abc import abstractmethod
from typing import List, Literal, Optional, Union

import pandas as pd
from pydantic import Field, field_validator
from typing_extensions import Annotated

from bofire.data_models.base import BaseModel
from bofire.data_models.domain.api import Domain


class EvaluateableCondition:
@abstractmethod
def evaluate(self, domain: Domain, experiments: Optional[pd.DataFrame]) -> bool:
pass


class Condition(BaseModel):
Expand All @@ -14,16 +23,28 @@ class SingleCondition(BaseModel):
type: str


class NumberOfExperimentsCondition(SingleCondition):
class NumberOfExperimentsCondition(SingleCondition, EvaluateableCondition):
type: Literal["NumberOfExperimentsCondition"] = "NumberOfExperimentsCondition"
n_experiments: Annotated[int, Field(ge=1)]

def evaluate(self, domain: Domain, experiments: Optional[pd.DataFrame]) -> bool:
if experiments is None:
n_experiments = 0
else:
n_experiments = len(
domain.outputs.preprocess_experiments_all_valid_outputs(experiments)
)
return n_experiments <= self.n_experiments


class AlwaysTrueCondition(SingleCondition):
class AlwaysTrueCondition(SingleCondition, EvaluateableCondition):
type: Literal["AlwaysTrueCondition"] = "AlwaysTrueCondition"

def evaluate(self, domain: Domain, experiments: Optional[pd.DataFrame]) -> bool:
return True

class CombiCondition(Condition):

class CombiCondition(Condition, EvaluateableCondition):
type: Literal["CombiCondition"] = "CombiCondition"
conditions: Annotated[
List[
Expand All @@ -41,3 +62,15 @@ def validate_n_required_conditions(cls, v, info):
"Number of required conditions larger than number of conditions."
)
return v

def evaluate(self, domain: Domain, experiments: Optional[pd.DataFrame]) -> bool:
n_matched_conditions = 0
for c in self.conditions:
if c.evaluate(domain, experiments):
n_matched_conditions += 1
if n_matched_conditions >= self.n_required_conditions:
return True
return False


AnyCondition = Union[NumberOfExperimentsCondition, CombiCondition, AlwaysTrueCondition]
14 changes: 5 additions & 9 deletions bofire/data_models/strategies/stepwise/stepwise.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from typing import List, Literal, Type, Union
from typing import List, Literal, Optional, Type, Union

from pydantic import Field, field_validator
from typing_extensions import Annotated

from bofire.data_models.base import BaseModel
from bofire.data_models.constraints.api import Constraint
from bofire.data_models.features.api import Feature
from bofire.data_models.strategies.api import AnyCondition
from bofire.data_models.strategies.doe import DoEStrategy
from bofire.data_models.strategies.factorial import FactorialStrategy
from bofire.data_models.strategies.predictives.mobo import MoboStrategy
Expand All @@ -21,12 +22,9 @@
from bofire.data_models.strategies.random import RandomStrategy
from bofire.data_models.strategies.shortest_path import ShortestPathStrategy
from bofire.data_models.strategies.space_filling import SpaceFillingStrategy
from bofire.data_models.strategies.stepwise.conditions import (
AlwaysTrueCondition,
CombiCondition,
NumberOfExperimentsCondition,
)
from bofire.data_models.strategies.stepwise.conditions import AlwaysTrueCondition
from bofire.data_models.strategies.strategy import Strategy
from bofire.data_models.transforms.api import AnyTransform

AnyStrategy = Union[
SoboStrategy,
Expand All @@ -44,14 +42,12 @@
ShortestPathStrategy,
]

AnyCondition = Union[NumberOfExperimentsCondition, CombiCondition, AlwaysTrueCondition]


class Step(BaseModel):
type: Literal["Step"] = "Step"
strategy_data: AnyStrategy
condition: AnyCondition
max_parallelism: Annotated[int, Field(ge=-1)] # -1 means no restriction at all
transform: Optional[AnyTransform] = None


class StepwiseStrategy(Strategy):
Expand Down
Empty file.
3 changes: 3 additions & 0 deletions bofire/data_models/transforms/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from bofire.data_models.transforms.drop_data import DropDataTransform

AnyTransform = DropDataTransform
9 changes: 9 additions & 0 deletions bofire/data_models/transforms/drop_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from typing import List, Literal, Optional

from pydantic import BaseModel


class DropDataTransform(BaseModel):
type: Literal["DropDataTransform"] = "DropDataTransform"
to_be_removed_experiments: Optional[List[int]] = None
to_be_removed_candidates: Optional[List[int]] = None
67 changes: 0 additions & 67 deletions bofire/strategies/stepwise/conditions.py

This file was deleted.

75 changes: 52 additions & 23 deletions bofire/strategies/stepwise/stepwise.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from typing import Dict, Tuple, Type
from typing import Dict, Literal, Optional, Tuple, Type, TypeVar, Union

import pandas as pd
from pydantic import PositiveInt

import bofire.data_models.strategies.api as data_models
import bofire.strategies.stepwise.conditions as conditions
from bofire.data_models.strategies.api import Step
import bofire.transforms.api as transforms
from bofire.data_models.domain.api import Domain
from bofire.data_models.strategies.api import StepwiseStrategy as data_model
from bofire.strategies.doe_strategy import DoEStrategy
from bofire.strategies.factorial import FactorialStrategy
Expand All @@ -22,6 +23,7 @@
from bofire.strategies.shortest_path import ShortestPathStrategy
from bofire.strategies.space_filling import SpaceFillingStrategy
from bofire.strategies.strategy import Strategy
from bofire.transforms.transform import Transform

# we have to duplicate the map functionality due to prevent circular imports
STRATEGY_MAP: Dict[Type[data_models.Strategy], Type[Strategy]] = {
Expand All @@ -41,40 +43,67 @@
}


def map(data_model: data_models.Strategy) -> Strategy:
def _map(data_model: data_models.Strategy) -> Strategy:
cls = STRATEGY_MAP[data_model.__class__]
return cls.from_spec(data_model=data_model)


T = TypeVar("T", pd.DataFrame, Domain)


TfData = Union[Literal["experiments"], Literal["candidates"], Literal["domain"]]


def _apply_tf(
data: Optional[T],
transform: Optional[Transform],
tf: TfData,
) -> Optional[T]:
if data is not None and transform is not None:
return getattr(transform, f"transform_{tf}")(data)


class StepwiseStrategy(Strategy):
def __init__(self, data_model: data_model, **kwargs):
super().__init__(data_model, **kwargs)
self.steps = data_model.steps
self.strategies = [_map(s.strategy_data) for s in data_model.steps]
self.conditions = [s.condition for s in data_model.steps]
self.transforms = [
s.transform and transforms.map(s.transform) for s in data_model.steps
]

def has_sufficient_experiments(self) -> bool:
return True

def _get_step(self) -> Tuple[int, Step]: # type: ignore
for i, step in enumerate(self.steps):
condition = conditions.map(step.condition)
def _get_step(self) -> Tuple[Strategy, Optional[Transform]]:
"""Returns the strategy at the current step and the corresponding transform if given."""
for i, condition in enumerate(self.conditions):
if condition.evaluate(self.domain, experiments=self.experiments):
return i, step
return self.strategies[i], self.transforms[i]
raise ValueError("No condition could be satisfied.")

def _ask(self, candidate_count: int) -> pd.DataFrame:
# we have to decide here w
istep, step = self._get_step()
if (step.max_parallelism > 0) and (candidate_count > step.max_parallelism):
raise ValueError(
f"Maximum number of candidates for step {istep} is {step.max_parallelism}."
)
# map it
strategy = map(step.strategy_data)
def _ask(self, candidate_count: Optional[PositiveInt]) -> pd.DataFrame:
strategy, transform = self._get_step()

candidate_count = candidate_count or 1

# handle a possible transform
tf_domain = _apply_tf(self.domain, transform, "domain")
transformed_domain = tf_domain or self.domain
strategy.domain = transformed_domain
tf_exp = _apply_tf(self.experiments, transform, "experiments")
transformed_experiments = self.experiments if tf_exp is None else tf_exp
tf_cand = _apply_tf(self.candidates, transform, "candidates")
transformed_candidates = self.candidates if tf_cand is None else tf_cand
# tell the experiments
if self.num_experiments > 0:
strategy.tell(experiments=self.experiments)
if transformed_experiments is not None and len(transformed_experiments) > 0:
strategy.tell(experiments=transformed_experiments, replace=True)
# tell pending
if self.num_candidates > 0:
strategy.set_candidates(self.candidates)
if transformed_candidates is not None and len(transformed_candidates) > 0:
strategy.set_candidates(transformed_candidates)
# ask and return
return strategy.ask(candidate_count=candidate_count)
candidates = strategy.ask(candidate_count=candidate_count)
if transform is not None:
return transform.untransform_candidates(candidates)
else:
return candidates
Empty file added bofire/transforms/__init__.py
Empty file.
13 changes: 13 additions & 0 deletions bofire/transforms/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from typing import Dict, Type

import bofire.data_models.transforms.api as data_models
from bofire.transforms.drop_data import DropDataTransform
from bofire.transforms.transform import Transform

TRANSFORM_MAP: Dict[Type[data_models.AnyTransform], Type[Transform]] = {
data_models.DropDataTransform: DropDataTransform
}


def map(data_model: data_models.AnyTransform) -> Transform:
return TRANSFORM_MAP[data_model.__class__](data_model)
16 changes: 16 additions & 0 deletions bofire/transforms/drop_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import pandas as pd

from bofire.data_models.transforms.api import DropDataTransform as DataModel
from bofire.transforms.transform import Transform


class DropDataTransform(Transform):
def __init__(self, data_model: DataModel):
self.to_be_removed_experiments = data_model.to_be_removed_experiments or []
self.to_be_removed_candidates = data_model.to_be_removed_candidates or []

def transform_experiments(self, experiments: pd.DataFrame) -> pd.DataFrame:
return experiments.drop(self.to_be_removed_experiments)

def transform_candidates(self, candidates: pd.DataFrame) -> pd.DataFrame:
return candidates.drop(self.to_be_removed_candidates)
20 changes: 20 additions & 0 deletions bofire/transforms/transform.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import pandas as pd

from bofire.data_models.domain.api import Domain


class Transform:
def __init__(self, *_args, **_kwargs) -> None:
pass

def transform_experiments(self, experiments: pd.DataFrame) -> pd.DataFrame:
return experiments

def transform_candidates(self, candidates: pd.DataFrame) -> pd.DataFrame:
return candidates

def transform_domain(self, domain: Domain) -> Domain:
return domain

def untransform_candidates(self, experiments: pd.DataFrame) -> pd.DataFrame:
return experiments
Loading

0 comments on commit 7909654

Please sign in to comment.