Skip to content

Commit

Permalink
Added support for continuous parameters sampling
Browse files Browse the repository at this point in the history
  • Loading branch information
mpvanderschelling committed May 13, 2024
1 parent d81181b commit 5be56da
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 14 deletions.
30 changes: 29 additions & 1 deletion src/f3dasm/_src/design/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,34 @@ def _check_range(self):
f"(lower_bound={self.lower_bound}, \
higher_bound={self.upper_bound}")

def to_discrete(self, step: int = 1) -> _DiscreteParameter:
"""Convert the continuous parameter to a discrete parameter.
Parameters
----------
step : int
The step size of the discrete search space, which defaults to 1.
Returns
-------
DiscreteParameter
The discrete parameter.
Raises
------
ValueError
If the step size is less than or equal to 0.
"""
if step <= 0:
raise ValueError("The step size must be larger than 0.")

return _DiscreteParameter(
lower_bound=int(self.lower_bound),
upper_bound=int(self.upper_bound),
step=step
)


@dataclass
class _DiscreteParameter(_Parameter):
Expand Down Expand Up @@ -251,7 +279,7 @@ def _check_range(self):
raise ValueError("step size must be larger than 0!")


@dataclass
@ dataclass
class _CategoricalParameter(_Parameter):
"""Create a search space parameter that is categorical
Expand Down
54 changes: 43 additions & 11 deletions src/f3dasm/_src/design/samplers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
else:
from typing import Literal

from typing import Optional
from typing import Dict, Optional

# Third-party
import numpy as np
Expand Down Expand Up @@ -107,7 +107,8 @@ def sample_continuous(self, numsamples: int) -> np.ndarray:
"""
raise NotImplementedError("Subclasses should implement this method.")

def get_samples(self, numsamples: Optional[int] = None) -> pd.DataFrame:
def get_samples(
self, numsamples: Optional[int] = None, **kwargs) -> pd.DataFrame:
"""Receive samples of the search space
Parameters
Expand Down Expand Up @@ -163,12 +164,12 @@ def get_samples(self, numsamples: Optional[int] = None) -> pd.DataFrame:

return df

def __call__(self, domain: Domain, n_samples: int, seed: int):
def __call__(self, domain: Domain, n_samples: int, seed: int, **kwargs):
"""Call the sampler"""
self.domain = domain
self.number_of_samples = n_samples
self.seed = seed
return self.get_samples()
return self.get_samples(**kwargs)

def _sample_constant(self, numsamples: int):
constant = self.domain.get_constant_parameters()
Expand Down Expand Up @@ -311,17 +312,33 @@ class GridSampler(Sampler):
"""

def get_samples(self, numsamples: Optional[int] = None) -> pd.DataFrame:
def get_samples(
self,
numsamples: Optional[int] = None,
stepsize_continuous_parameters:
Optional[Dict[str, float] | float] = None,
**kwargs) -> pd.DataFrame:
"""Receive samples of the search space
Parameters
----------
numsamples
numsamples : int
number of samples
stepsize_continuous_parameters : Dict[str, float] | float, optional
stepsize for the continuous parameters, by default None.
If a float is given, all continuous parameters are sampled with
the same stepsize. If a dictionary is given, the stepsize for each
continuous parameter can be specified.
Returns
-------
Data objects with the samples
Raises
------
ValueError
If the stepsize_continuous_parameters is given as a dictionary
and not specified for all continuous parameters.
"""

self.set_seed(self.seed)
Expand All @@ -332,10 +349,21 @@ def get_samples(self, numsamples: Optional[int] = None) -> pd.DataFrame:

continuous = self.domain.get_continuous_parameters()

if continuous:
raise ValueError("Grid sampling is only possible for domains \
strictly with only discrete and \
categorical parameters")
if isinstance(stepsize_continuous_parameters, float):
continuous = {key: continuous[key].to_discrete(
step=stepsize_continuous_parameters) for key in continuous}

elif isinstance(stepsize_continuous_parameters, dict):
continuous = {key: continuous[key].to_discrete(
step=value) for key,
value in stepsize_continuous_parameters.items()}

if len(continuous) != len(
self.domain.get_continuous_parameters()):
raise ValueError(
"If you specify the stepsize for continuous parameters, \
the stepsize_continuous_parameters should \
contain all continuous parameters")

discrete = self.domain.get_discrete_parameters()
categorical = self.domain.get_categorical_parameters()
Expand All @@ -346,7 +374,11 @@ def get_samples(self, numsamples: Optional[int] = None) -> pd.DataFrame:
_iterdict[k] = v.categories

for k, v, in discrete.items():
_iterdict[k] = range(v.lower_bound, v.upper_bound+1)
_iterdict[k] = range(v.lower_bound, v.upper_bound+1, v.step)

for k, v, in continuous.items():
_iterdict[k] = np.arange(
start=v.lower_bound, stop=v.upper_bound, step=v.step)

return pd.DataFrame(list(product(*_iterdict.values())),
columns=_iterdict, dtype=object)
15 changes: 13 additions & 2 deletions src/f3dasm/_src/experimentdata/experimentdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -1708,7 +1708,7 @@ def _iterate_scipy(self, optimizer: Optimizer,
# =========================================================================

def sample(self, sampler: Sampler | SamplerNames, n_samples: int = 1,
seed: Optional[int] = None) -> None:
seed: Optional[int] = None, **kwargs) -> None:
"""Sample data from the domain providing the sampler strategy
Parameters
Expand All @@ -1726,6 +1726,17 @@ def sample(self, sampler: Sampler | SamplerNames, n_samples: int = 1,
seed : Optional[int], optional
Seed to use for the sampler, by default None
Note
----
When using the 'grid' sampler, an optional argument
'stepsize_continuous_parameters' can be passed to specify the stepsize
to cast continuous parameters to discrete parameters.
- The stepsize should be a dictionary with the parameter names as keys\
and the stepsize as values.
- Alternatively, a single stepsize can be passed for all continuous\
parameters.
Raises
------
ValueError
Expand All @@ -1736,7 +1747,7 @@ def sample(self, sampler: Sampler | SamplerNames, n_samples: int = 1,
sampler = _sampler_factory(sampler, self.domain)

sample_data: DataTypes = sampler(
domain=self.domain, n_samples=n_samples, seed=seed)
domain=self.domain, n_samples=n_samples, seed=seed, **kwargs)
self.add(input_data=sample_data, domain=self.domain)

# Project directory
Expand Down
29 changes: 29 additions & 0 deletions tests/design/test_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,5 +177,34 @@ def test_add_combination(args):
assert a + b == expected


def test_to_discrete():
a = _ContinuousParameter(0., 5.)
c = _DiscreteParameter(0, 5, 0.2)
b = a.to_discrete(0.2)
assert isinstance(b, _DiscreteParameter)
assert b.lower_bound == 0
assert b.upper_bound == 5
assert b.step == 0.2
assert b == c


def test_to_discrete_negative_stepsize():
a = _ContinuousParameter(0., 5.)
with pytest.raises(ValueError):
a.to_discrete(-0.2)


def test_default_stepsize_to_discrete():
default_stepsize = 1
a = _ContinuousParameter(0., 5.)
c = _DiscreteParameter(0, 5, default_stepsize)
b = a.to_discrete()
assert isinstance(b, _DiscreteParameter)
assert b.lower_bound == 0
assert b.upper_bound == 5
assert b.step == default_stepsize
assert b == c


if __name__ == "__main__": # pragma: no cover
pytest.main()

0 comments on commit 5be56da

Please sign in to comment.