Skip to content

Commit

Permalink
Prepare for irace v4
Browse files Browse the repository at this point in the history
  • Loading branch information
Saethox committed Oct 4, 2024
1 parent 7f784cf commit a78bef8
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 118 deletions.
4 changes: 2 additions & 2 deletions examples/dual_annealing.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ def target_runner(experiment: Experiment, scenario: Scenario) -> float:
scenario = Scenario(
max_experiments=180,
instances=[Rastrigin(dim) for dim in (2, 3, 5, 10, 20, 40)],
verbose=1,
verbose=100,
seed=42,
)

if __name__ == '__main__':
result = irace(target_runner, scenario, parameter_space, return_df=True)
result = irace(target_runner, parameter_space, scenario, return_df=True)
print(result)
45 changes: 30 additions & 15 deletions examples/parameter_space.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
from irace import ParameterSpace, Categorical, Real, Integer, Bool
import irace.params as p
from irace import ParameterSpace, Categorical, Real, Integer, Bool, Scenario, Experiment, irace

parameter_space = ParameterSpace([
Categorical('algorithm', ['as', 'mmas', 'eas', 'ras', 'acs']),
Categorical('localsearch', [0, 1, 2, 3]),
Real('alpha', 0, 5),
Real('beta', 0, 10),
Real('rho', 0.01, 1),
Integer('ants', 5, 100),
Integer('nnls', 5, 50),
Real('q0', 0, 1, condition=p.ValueOf('algorithm').eq('acs')),
Integer('rasrank', 1, p.ValueOf('ants').min(10), condition=p.ValueOf('algorithm').eq('ras')),
Integer('elistants', 1, p.ValueOf('ants')),
Integer('nnls', 5, 50, condition=p.ValueOf('localsearch').isin([1, 2, 3])),
Bool('dlb', condition=p.ValueOf('localsearch').isin([1, 2, 3])),
], forbidden=[p.all(p.ValueOf('alpha').eq(0), p.ValueOf('beta').eq(0))])

scenario = Scenario(
max_experiments=300,
verbose=100,
seed=42,
)

if __name__ == '__main__':
parameter_space = ParameterSpace([
Categorical('algorithm', ['as', 'mmas', 'eas', 'ras', 'acs']),
Categorical('localsearch', [0, 1, 2, 3]),
Real('alpha', 0, 5),
Real('beta', 0, 10),
Real('rho', 0.01, 1),
Integer('ants', 5, 100),
Integer('nnls', 5, 50),
Real('q0', 0, 1),
Bool('dlb'),
Integer('rasrank', 1, "ants"),
Integer('elistants', 1, 750),
])

def target_runner(experiment: Experiment, _) -> float:
return experiment.configuration['alpha'] * experiment.configuration['beta']


if __name__ == '__main__':
print(parameter_space)
result = irace(target_runner, parameter_space, scenario, return_df=True)
print(result)
109 changes: 79 additions & 30 deletions irace/_rpy2.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import logging
import math
from collections import OrderedDict
from typing import Any
from collections.abc import Mapping, Collection
from typing import Any, Optional

import numpy as np
import pandas as pd
from rpy2 import rinterface, robjects, rinterface_lib
from rpy2.rinterface import SexpClosure, ListSexpVector, rternalize
from rpy2.robjects import ListVector
from rpy2.robjects import ListVector, IntVector, BoolVector, RObject
from rpy2.robjects import numpy2ri, pandas2ri
from rpy2.robjects.packages import importr, PackageNotInstalledError

from . import params as p
from .experiment import Experiment
from .params import ParameterSpace, Real, Integer, Bool, Categorical, Ordinal
from .params import ParameterSpace
from .runner import TargetRunner
from .scenario import Scenario

Expand All @@ -36,9 +38,9 @@ def rpy2py_recursive(data: Any) -> Any:
Leaves will be converted to e.g. numpy arrays or lists as appropriate and the whole tree to a dictionary.
"""

if data == rinterface.NULL:
return None
elif data == rinterface.na_values.NA_Character:
if data in (
rinterface.NULL, rinterface.NA_Character, rinterface.NA_Real, rinterface.NA_Integer,
rinterface.NA_Logical, rinterface.NA):
return None
elif type(data) in [robjects.DataFrame, robjects.ListVector]:
return OrderedDict(zip(data.names, [rpy2py_recursive(elt) for elt in data]))
Expand All @@ -47,7 +49,7 @@ def rpy2py_recursive(data: Any) -> Any:
return rpy2py_recursive(data[0])
else:
return [rpy2py_recursive(elt) for elt in data]
elif type(data) in [robjects.FloatVector, robjects.IntVector]:
elif type(data) in [robjects.FloatVector, robjects.IntVector, robjects.BoolVector]:
if len(data) == 1:
return rpy2py_recursive(data[0])
else:
Expand All @@ -71,14 +73,18 @@ def convert_configuration(raw_configuration: dict[str, Any], parameter_space: Pa
if subspace is None:
continue

if isinstance(subspace, Real):
if raw_param is None or (isinstance(raw_param, float) and math.isnan(raw_param)):
configuration[name] = None
continue

if isinstance(subspace, p.Real):
param = float(raw_param)
elif isinstance(subspace, Integer):
elif isinstance(subspace, p.Integer):
param = int(raw_param)
elif isinstance(subspace, Bool):
# `bool` is represented as discrete with `["0", "1"]` variants.
param = bool(int(raw_param))
elif isinstance(subspace, Categorical) or isinstance(subspace, Ordinal):
elif isinstance(subspace, p.Bool):
# `bool` is represented as discrete with `["TRUE", "FALSE"]` variants.
param = bool(raw_param)
elif isinstance(subspace, p.Categorical) or isinstance(subspace, p.Ordinal):
# categorical and ordinal are represented as integers, so we need to convert to the real variant.
param = subspace.values[int(raw_param)]
else:
Expand Down Expand Up @@ -106,11 +112,11 @@ def convert_result(result: pd.DataFrame, parameter_space: ParameterSpace, return
def rpy2py_experiment(obj: ListVector, scenario: Scenario, parameter_space: ParameterSpace) -> Experiment:
experiment = rpy2py_recursive(obj)

configuration_id = str(experiment['id.configuration'])
configuration_id = str(experiment['id_configuration'])
seed = int(experiment['seed'])

if scenario.instances is not None:
instance_id = str(experiment['id.instance'])
instance_id = str(experiment['id_instance'])
instance = scenario.instances[int(experiment['instance'])]
else:
instance_id = None
Expand All @@ -129,8 +135,46 @@ def rpy2py_experiment(obj: ListVector, scenario: Scenario, parameter_space: Para
return experiment


def py2rpy_expression(value: Any) -> RObject:
return robjects.r(f'expression({p.check_expression(value)})')


def py2rpy_quote(value: Any) -> RObject:
return robjects.r(f'quote({p.check_expression(value)})')


def py2rpy_parameter_space(parameter_space: ParameterSpace) -> ListVector:
return _irace.readParameters(text=str(parameter_space))
r_parameter_space = []
for subspace in parameter_space.params.values():
if subspace.condition is not None:
condition = py2rpy_expression(subspace.condition)
else:
condition = True
if isinstance(subspace, p.Real) or isinstance(subspace, p.Integer):
constructor = _irace.param_real if isinstance(subspace, p.Real) else _irace.param_int
lower = py2rpy_expression(subspace.lower)
upper = py2rpy_expression(subspace.upper)
transf = "log" if subspace.log else ""
r_subspace = constructor(name=subspace.name, lower=lower, upper=upper, condition=condition, transf=transf)
elif isinstance(subspace, p.Bool):
# `bool` is represented as discrete with `["0", "1"]` variants.
values = BoolVector([False, True])
r_subspace = _irace.param_cat(subspace.name, values=values, condition=condition)
elif isinstance(subspace, p.Categorical) or isinstance(subspace, p.Ordinal):
# categorical and ordinal are represented as integers.
values = IntVector(list(range(len(subspace.values))))
r_subspace = _irace.param_cat(subspace.name, values=values, condition=condition)
else:
raise ValueError("unknown parameter type")

r_parameter_space.append(r_subspace)

if parameter_space.forbidden is not None:
forbidden = py2rpy_expression(p.any(*parameter_space.forbidden))
else:
forbidden = ''

return _irace.parametersNew(*r_parameter_space, forbidden=forbidden)


def py2rpy_target_runner(target_runner: TargetRunner, scenario: Scenario,
Expand All @@ -142,24 +186,26 @@ def inner(experiment: ListSexpVector, _: ListSexpVector) -> ListVector:
experiment = rpy2py_experiment(ListVector(experiment), scenario, parameter_space)

try:
result = target_runner(experiment=experiment, scenario=scenario)
result = target_runner(experiment, scenario)

if isinstance(result, Collection) and len(result) == 2:
cost, time = result
r_result = ListVector(dict(cost=float(cost), time=float(time)))
elif isinstance(result, Mapping):
r_result = ListVector({key: float(value) for key, value in result.items()})
else:
r_result = ListVector(dict(cost=float(result)))

except Exception as e:
return ListVector(dict(cost=math.inf, error=str(e)))

if isinstance(result, float):
return ListVector(dict(cost=float(result)))
elif isinstance(result, tuple):
cost, time = result
return ListVector(dict(cost=float(cost), time=float(time)))
elif isinstance(result, dict):
return ListVector({key: float(value) for key, value in result.items()})
else:
raise NotImplementedError("`target_runner` returned an invalid result")
r_result = ListVector(dict(cost=math.inf, error=str(e)))

return r_result

return inner


def py2rpy_scenario(scenario: Scenario, r_target_runner: SexpClosure) -> ListVector:
def py2rpy_scenario(scenario: Scenario, r_target_runner: SexpClosure,
r_parameter_space: Optional[ListVector] = None) -> ListVector:
r_scenario = {
'targetRunner': r_target_runner,
'elitist': int(scenario.elitist),
Expand All @@ -169,6 +215,9 @@ def py2rpy_scenario(scenario: Scenario, r_target_runner: SexpClosure) -> ListVec
'parallel': scenario.n_jobs,
}

if r_parameter_space is not None:
r_scenario['parameters'] = r_parameter_space

if scenario.max_experiments is not None:
r_scenario['maxExperiments'] = scenario.max_experiments

Expand All @@ -192,4 +241,4 @@ def py2rpy_scenario(scenario: Scenario, r_target_runner: SexpClosure) -> ListVec
if scenario.seed is not None:
r_scenario['seed'] = scenario.seed

return _irace.checkScenario(ListVector(r_scenario))
return ListVector(r_scenario)
17 changes: 8 additions & 9 deletions irace/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,30 @@
from .scenario import Scenario


def irace(target_runner: TargetRunner, scenario: Scenario, parameter_space: ParameterSpace, return_df: bool = False,
def irace(target_runner: TargetRunner, parameter_space: ParameterSpace, scenario: Scenario, return_df: bool = False,
remove_metadata: bool = True) -> pd.DataFrame | list[dict[str, Any]]:
"""irace: Iterated Racing for Automatic Algorithm Configuration."""

from ._rpy2 import _irace, py2rpy_scenario, py2rpy_target_runner, \
py2rpy_parameter_space, converter, convert_result
from ._rpy2 import _irace, py2rpy_scenario, py2rpy_target_runner, py2rpy_parameter_space, converter, convert_result

r_target_runner = py2rpy_target_runner(target_runner, scenario, parameter_space)
r_scenario = py2rpy_scenario(scenario, r_target_runner)
r_parameter_space = py2rpy_parameter_space(parameter_space)
r_scenario = py2rpy_scenario(scenario, r_target_runner, r_parameter_space)

result = _irace.irace(r_scenario, r_parameter_space)
result = _irace.irace(r_scenario)
result = converter.rpy2py(result)

return convert_result(result, parameter_space, return_df=return_df, remove_metadata=remove_metadata)


class Run:
"""A single run of irace with a given target runner, scenario and parameter space."""
"""A single run of irace with a given target runner and scenario."""

def __init__(self, target_runner: TargetRunner, scenario: Scenario, parameter_space: ParameterSpace,
def __init__(self, target_runner: TargetRunner, parameter_space: ParameterSpace, scenario: Scenario,
name: Optional[str] = None) -> None:
self.target_runner = target_runner
self.scenario = scenario
self.parameter_space = parameter_space
self.scenario = scenario
self.name = name


Expand All @@ -45,7 +44,7 @@ def multi_irace(runs: Iterable[Run], n_jobs: int = 1, return_df: bool = False, r

@delayed
def inner(run: Run) -> pd.DataFrame | list[dict[str, Any]]:
return irace(target_runner=run.target_runner, scenario=run.scenario, parameter_space=run.parameter_space,
return irace(target_runner=run.target_runner, scenario=run.scenario,
return_df=return_df, remove_metadata=remove_metadata)

results = Parallel(n_jobs=n_jobs)(inner(run) for run in runs)
Expand Down
19 changes: 3 additions & 16 deletions irace/experiment.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,13 @@
from dataclasses import dataclass
from typing import Any, Optional


@dataclass
class Experiment:
"""Metadata about the current experiment i.e. target runner execution."""
"""Metadata about the current experiment, i.e. target runner execution."""

configuration_id: str
instance_id: Optional[str]
instance: Optional[Any]
seed: int
bound: int
configuration: dict[str, Any]

def __init__(
self,
configuration_id: str,
instance_id: Optional[str],
instance: Optional[Any],
seed: int,
configuration: dict[str, Any],
) -> None:
self.configuration_id = configuration_id
self.instance_id = instance_id
self.seed = seed
self.instance = instance
self.configuration = configuration
Loading

0 comments on commit a78bef8

Please sign in to comment.