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

add new parameter programmic interface #25

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
22 changes: 22 additions & 0 deletions examples/parameters_definition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from irace import Symbol, Parameters, Param, Integer, Real, Categorical, Ordinal, Min, In, List

from rpy2.robjects.packages import importr
base = importr('base')
parameters = Parameters()

parameters.algorithm = Param(Categorical(('as', 'mmas', 'eas', 'ras', 'acs')))
parameters.localsearch = Param(Categorical(('0', '1', '2', '3')))
parameters.alpha = Param(Real(0, 5))
parameters.beta = Param(Real(0, 10))
parameters.rho = Param(Real(0.01, 1))
parameters.ants = Param(Integer(5, 100, log=True))
parameters.q0 = Param(Real(0, 1), condition=Symbol('algorithm') == "acs")
parameters.rasrank = Param(Integer(1, Min(Symbol('ants'), 10)), condition=Symbol('algorithm') == 'ras')
parameters.elitistants = Param(Integer(1, Symbol('ants')), condition=Symbol('algorithm') == 'eas')
parameters.nnls = Param(Integer(5, 50), condition=List((1, 2, 3)).contains(Symbol('localsearch')))
parameters.dlbs = Param(Integer(5, 50), condition=List((1, 2, 3)).contains(Symbol('localsearch')))


a = parameters._export()

base.print(a)
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ dependencies = [
"numpy",
"rpy2 >= 3.5.6",
"scipy",
"pandas >= 1.0.0"
"pandas >= 1.0.0",
"ConfigSpace"
]

[projects.urls]
Expand Down
17 changes: 14 additions & 3 deletions src/irace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pandas as pd
import traceback
import warnings
from typing import Union

import rpy2.robjects as ro
from rpy2.robjects.packages import importr, PackageNotInstalledError
Expand All @@ -15,6 +16,11 @@
from rpy2.robjects.vectors import DataFrame, BoolVector, FloatVector, IntVector, StrVector, ListVector, IntArray, Matrix, ListSexpVector,FloatSexpVector,IntSexpVector,StrSexpVector,BoolSexpVector
from rpy2.robjects.functions import SignatureTranslatedFunction
from rpy2.rinterface import RRuntimeWarning
from .errors import irace_assert

# Re export useful Functions
from .expressions import Symbol, Min, Max, Round, Floor, Ceiling, Trunc, In, List
from .parameters import Integer, Real, Ordinal, Categorical, Param, Parameters

rpy2conversion = ro.conversion.get_conversion()
irace_converter = ro.default_converter + numpy2ri.converter + pandas2ri.converter
Expand Down Expand Up @@ -105,12 +111,17 @@ class irace:
except PackageNotInstalledError as e:
raise PackageNotInstalledError('The R package irace needs to be installed for this python binding to work. Consider running `Rscript -e "install.packages(\'irace\', repos=\'https://cloud.r-project.org\')"` in your shell. See more details at https://github.com/mLopez-Ibanez/irace#quick-start') from e

def __init__(self, scenario, parameters_table, target_runner):
def __init__(self, scenario, parameters: Union[Parameters, str], target_runner):
self.scenario = scenario
if 'instances' in scenario:
self.scenario['instances'] = np.asarray(scenario['instances'])
with localconverter(irace_converter_hack):
self.parameters = self._pkg.readParameters(text = parameters_table, digits = self.scenario.get('digits', 4))
if isinstance(parameters, Parameters):
self.parameters = parameters._export()
elif isinstance(parameters, str):
with localconverter(irace_converter_hack):
self.parameters = self._pkg.readParameters(text = parameters, digits = self.scenario.get('digits', 4))
else:
raise ValueError(f"parameters needs to be type irace.Parameters or string, but {type(parameters)} is found.")
self.context = {'py_target_runner' : target_runner,
'py_scenario': self.scenario }
check_windows(scenario)
Expand Down
77 changes: 77 additions & 0 deletions src/irace/compatibility/config_space.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from ..errors import irace_assert, check_illegal_character
import re
from ConfigSpace.hyperparameters import CategoricalHyperparameter, OrdinalHyperparameter, IntegerHyperparameter, FloatHyperparameter
from ..parameters import Categorical, Ordinal, Real, Integer, Parameters, Param
from ..expressions import And, Or, Eq, Not, Lt, Gt, Symbol, List
from ConfigSpace.conditions import EqualsCondition, NotEqualsCondition, LessThanCondition, GreaterThanCondition, InCondition, AndConjunction, OrConjunction

def check_parameter_name(name):
check_illegal_character(name)
irace_assert(not (re.match('^__.*__$', name) or re.match('^_export$', name)), f"Unfortunately, your name parameter {repr(name)} clashes with reserved names, which are '__.*__' and '_export'. Please rename the name.")

def convert_from_config_space(config_space):
parameters = Parameters()
for cf_param_name in config_space:
check_parameter_name(cf_param_name)
cf_param = config_space[cf_param_name]
if isinstance(cf_param, CategoricalHyperparameter):
param = Param(Categorical(cf_param.choices))
elif isinstance(cf_param, OrdinalHyperparameter):
param = Param(Ordinal(cf_param.sequence))
elif isinstance(cf_param, IntegerHyperparameter):
param = Param(Integer(cf_param.lower, cf_param.upper, log=cf_param.log))
elif isinstance(cf_param, FloatHyperparameter):
param = Param(Real(cf_param.lower, cf_param.upper, log=cf_param.log))
else:
raise NotImplementedError(f"parameter type {type(cf_param)} is currently not supported. If you are nice enough, please open an issue at https://github.com/auto-optimization/iracepy/issues.")

setattr(parameters, cf_param_name, param)

for name_symbol, condition in translate_conditions(config_space):
getattr(parameters, name_symbol.name).set_condition(condition)

return parameters

def translate_condition(config_space_condition):
condition = config_space_condition
if isinstance(condition, EqualsCondition):
left = Symbol(condition.get_parents()[0].name)
right = condition.value
return Eq(left, right)
elif isinstance(condition, NotEqualsCondition):
left = Symbol(condition.get_parents()[0].name)
right = condition.value
return Not(Eq(left, right))
elif isinstance(condition, LessThanCondition):
left = Symbol(condition.get_parents()[0].name)
right = condition.value
return Lt(left, right)
elif isinstance(condition, GreaterThanCondition):
left = Symbol(condition.get_parents()[0].name)
right = condition.value
return Gt(left, right)
elif isinstance(condition, InCondition):
left = Symbol(condition.get_parents()[0].name)
right = List(condition.value)
return right.contains(left)
elif isinstance(condition, AndConjunction):
elements = condition.components
irace_assert(len(elements) >= 2, "And condition has less than two elements?")
res = And(translate_condition(elements[0]), translate_condition(elements[1]))
for i in range(2, len(elements)):
res = And(translate_condition(elements[i]), res)
return res
elif isinstance(condition, OrConjunction):
elements = condition.components
irace_assert(len(elements) >= 2, "Or condition has less than two elements?")
res = Or(translate_condition(elements[0]), translate_condition(elements[1]))
for i in range(2, len(elements)):
res = Or(translate_condition(elements[i]), res)
return res


def translate_conditions(config_space):
con = config_space.get_conditions()
for condition in con:
name = condition.get_children()[0].name
yield Symbol(name), translate_condition(condition)
14 changes: 14 additions & 0 deletions src/irace/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import re

def irace_assert(condition, message):
# Currently just a plain assert. In the future we might add logging and change assertion error behavior to include cleanup, etc.
assert condition, message

def check_numbers(start, end, log):
irace_assert(not ((type(start) is int or float) and (type(end) is int or float)) or end >= start, f"lower bound must be smaller than upper bound in numeric range ({start}, {end})")
if log:
if (type(start) is int or float) and start <= 0 or (type(end) is int or float) and end <= 0:
irace_assert("Domain of type 'log' cannot be non-positive")

def check_illegal_character(name):
irace_assert(re.match("^[_a-zA-Z0-9]+$", name), f"name {repr(name)} container illegal character. THe only allowed characters are a-z, A-Z, 0-9 and _ (underscore).")
Loading