diff --git a/examples/parameters_definition.py b/examples/parameters_definition.py new file mode 100644 index 0000000..7a6ae50 --- /dev/null +++ b/examples/parameters_definition.py @@ -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) diff --git a/pyproject.toml b/pyproject.toml index 3b54ebc..5b20649 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,8 @@ dependencies = [ "numpy", "rpy2 >= 3.5.6", "scipy", - "pandas >= 1.0.0" + "pandas >= 1.0.0", + "ConfigSpace" ] [projects.urls] diff --git a/src/irace/__init__.py b/src/irace/__init__.py index a4858eb..3863a14 100644 --- a/src/irace/__init__.py +++ b/src/irace/__init__.py @@ -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 @@ -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 @@ -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) diff --git a/src/irace/compatibility/config_space.py b/src/irace/compatibility/config_space.py new file mode 100644 index 0000000..06d6022 --- /dev/null +++ b/src/irace/compatibility/config_space.py @@ -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) diff --git a/src/irace/errors.py b/src/irace/errors.py new file mode 100644 index 0000000..eb07422 --- /dev/null +++ b/src/irace/errors.py @@ -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).") diff --git a/src/irace/expressions.py b/src/irace/expressions.py new file mode 100644 index 0000000..9e21bb6 --- /dev/null +++ b/src/irace/expressions.py @@ -0,0 +1,251 @@ +from .errors import irace_assert, check_illegal_character +import re +from typing import Iterable +from rpy2.robjects.packages import importr +from rpy2 import robjects +from rpy2.rinterface import evalr_expr + +rbase = importr('base') + +robjects.r(''' + dputpy <- function(x) { + text_con <- textConnection("output", "w") + dput(x, text_con) + close(text_con) + paste(output, collapse = '') + } ''') + +dputpy = robjects.globalenv['dputpy'] + +class Expr: + def __init__(self): + pass + + def __hash__(self): + return hash(repr(self)) + + @classmethod + def raw_r(cls, expr_str): + return rbase.eval(rbase.parse(text = 'expression(' + expr_str + ')')) + + def export(self): + return self.raw_r(repr(self)) + + def __eq__(self, other): + return Eq(self, other) + + def __ne__(self, other): + return Ne(self, other) + + def __ge__(self, other): + return Ge(self, other) + + def __gt__(self, other): + return Gt(self, other) + + def __le__(self, other): + return Le(self, other) + + def __lt__(self, other): + return Lt(self, other) + + def __and__(self, other): + return And(self, other) + + def __not__(self): + return Not(self) + + def __add__(self, other): + return Add(self, other) + + def __sub__(self, other): + return Sub(self, other) + + def __mul__(self, other): + return Mul(self, other) + + def __truediv__(self, other): + return Div(self, other) + + def __mod__(self, other): + return Mod(self, other) + +class List: + def __init__(self, element: Iterable): + lst = list(element) + irace_assert(all(isinstance(x, int) for x in lst) or all(isinstance(x, float) for x in lst) or all(isinstance(x, str) for x in lst), "List must be all integers, all floating points or all strings.") + self.data = lst + + def __repr__(self): + return str(dputpy(self.export())[0]) + + def export(self): + if all(isinstance(x, int) for x in self.data): + return robjects.IntVector(self.data) + elif all(isinstance(x, float) for x in self.data): + return robjects.FloatVector(self.data) + elif all(isinstance(x, str) for x in self.data): + return robjects.StrSexpVector(self.data) + else: + raise ValueError("List must be all integers, all floating points or all strings.") + + def contains(self, element): + return In(element, self) + +class Singular(Expr): + def __init__(self, element): + self.element = element + self.left_op = '' + self.right_op = '' + + def __repr__(self): + return "(" + self.left_op + repr(self.element) + self.right_op + ")" + + +class BinaryRelations(Expr): + def __init__(self, left, right): + self.left = left + self.right = right + self.left_op = '' + self.center_op = '' + self.right_op = '' + + def __repr__(self): + return "(" + self.left_op + repr(self.left) + self.center_op + repr(self.right) + self.right_op + ")" + +class Symbol(Singular): + def __init__(self, name): + check_illegal_character(name) + super().__init__(name) + + def __repr__(self): + return self.element + + @property + def name(self): + return self.element + + +class True_(Expr): + def __init__(self) -> None: + pass + + def export(self): + return rbase.eval(rbase.parse(text = 'TRUE')) + +class Not(Singular): + def __init__(self, element): + super().__init__(element) + self.left_op = '!' + +class Symmetric(BinaryRelations): + def __init__(self, left, right): + super().__init__(left, right) + +class Eq(Symmetric): + def __init__(self, left, right): + super().__init__(left, right) + self.center_op = '==' + +class Ge(BinaryRelations): + def __init__(self, left, right): + super().__init__(left, right) + self.center_op = '>=' + +class Gt(BinaryRelations): + def __init__(self, left, right): + super().__init__(left, right) + self.center_op = '>' + +class Le(BinaryRelations): + def __init__(self, left, right): + super().__init__(left, right) + self.center_op = '<=' + +class Lt(BinaryRelations): + def __init__(self, left, right): + super().__init__(left, right) + self.center_op = '<' + +class Ne(Symmetric): + def __init__(self, left, right): + super().__init__(left, right) + self.center_op = '!=' + +class Or(Symmetric): + def __init__(self, left, right): + super().__init__(left, right) + self.center_op = '|' + +class And(Symmetric): + def __init__(self, left, right): + super().__init__(left, right) + self.center_op = '&' + +class In(BinaryRelations): + def __init__(self, left, right): + irace_assert(isinstance(right, List), f"expected a irace.expressions.List, but found {type(right)}") + super().__init__(left, right) + self.center_op = '%in%' + +class BinaryNamedFunction(BinaryRelations): + def __init__(self, left, right, name): + super().__init__(left, right) + self.left_op = name + '(' + self.center_op = ',' + self.right_op = ')' + +class SingularNamedFunction(Singular): + def __init__(self, element, name): + super().__init__(element) + self.left_op = name +'(' + self.right_op = ')' + +class Min(BinaryNamedFunction): + def __init__(self, left, right): + super().__init__(left, right, 'min') + +class Max(BinaryNamedFunction): + def __init__(self, left, right): + super().__init__(left, right, 'max') + +class Mul(BinaryRelations): + def __init__(self, left, right): + super().__init__(left, right) + self.center_op = '*' + +class Add(BinaryRelations): + def __init__(self, left, right): + super().__init__(left, right) + self.center_op = '+' + +class Sub(BinaryRelations): + def __init__(self, left, right): + super().__init__(left, right) + self.center_op = '-' + +class Div(BinaryRelations): + def __init__(self, left, right): + super().__init__(left, right) + self.center_op = '/' + +class Mod(BinaryRelations): + def __init__(self, left, right): + super().__init__(left, right) + self.center_op = '%%' + +class Round(SingularNamedFunction): + def __init__(self, element): + super().__init__(element, 'round') + +class Floor(SingularNamedFunction): + def __init__(self, element): + super().__init__(element, 'floor') + +class Ceiling(SingularNamedFunction): + def __init__(self, element): + super().__init__(element, 'ceiling') + +class Trunc(SingularNamedFunction): + def __init__(self, element): + super().__init__(element, 'trunc') diff --git a/src/irace/parameters.py b/src/irace/parameters.py new file mode 100644 index 0000000..52e5455 --- /dev/null +++ b/src/irace/parameters.py @@ -0,0 +1,124 @@ +from enum import Enum +from typing import Iterable, Union +from .errors import irace_assert, check_numbers +import re +from rpy2.robjects.packages import importr +from rpy2.robjects.vectors import StrVector, IntArray, FloatArray +from rpy2.rlike.container import TaggedList +from .expressions import Expr, True_ + +base = importr('base') +irace_pkg = importr('irace') + +class ParameterType(Enum): + INTEGER = 'i' + REAL = 'r' + ORDINAL = 'o' + CATEGORICAL = 'c' + INTEGER_LOG = 'i,log' + REAL_LOG = 'r,log' + +class ParameterDomain: + pass + +class Integer(ParameterDomain): + def __init__(self, start, end, log=False): + self.start = start + self.end = end + self.type = ParameterType.INTEGER_LOG if log else ParameterType.INTEGER + check_numbers(start, end, log) + irace_assert((isinstance(start, int) or isinstance(start, Expr)) and (isinstance(end, int) or isinstance(end, Expr)), "bounds must be integers or expressions") + + def export(self): + if isinstance(self.start, Expr) or isinstance(self.end, Expr): + return base.eval(base.parse(text = 'expression(' + repr(self.start) + ',' + repr(self.end) + ')')) + else: + return IntArray((self.start, self.end)) + +class Real(ParameterDomain): + def __init__(self, start, end, log=False): + start = float(start) if isinstance(start, int) else start + self.start = start + end = float(end) if isinstance(end, int) else end + self.end = end + self.type = ParameterType.REAL_LOG if log else ParameterType.REAL + check_numbers(start, end, log) + irace_assert((isinstance(start, float) or isinstance(start, Expr)) and (isinstance(end, float) or isinstance(end, Expr)), "bounds must be numbers or expressions") + + def export(self): + if isinstance(self.start, Expr) or isinstance(self.end, Expr): + # FIXME: Ideally floating point should not be converted to string because it will lose precision, but this seems to be the only way to construct an expression. + return base.eval(base.parse(text = 'expression(' + repr(self.start) + ',' + repr(self.end) + ')')) + else: + return FloatArray((self.start, self.end)) + +class Categorical(ParameterDomain): + def __init__(self, domain: Iterable = None): + if domain: + self.domain = list(domain) + self.type = ParameterType.CATEGORICAL + irace_assert(len(set(domain)) == len(list(domain)), "domain has duplicate elements") + for d in domain: + irace_assert(isinstance(d, Expr) or isinstance(d, str), "domain element must be either string or expression (irace.expressions.Expr)") + else: + self.domain = list() + + def add_element(self, element): + self.domain.append(element) + + def export(self): + self.domain = list(map(lambda x: repr(x) if isinstance(x, Expr) else x, self.domain)) + return StrVector(self.domain) + +class Ordinal(ParameterDomain): + def __init__(self, domain: Iterable = None): + if domain: + self.domain = list(domain) + self.type = ParameterType.ORDINAL + for d in domain: + irace_assert(isinstance(d, Expr) or isinstance(d, str), "domain element must be either string or expression (irace.expressions.Expr)") + irace_assert(len(set(self.domain)) == len(self.domain), "domain has duplicate elements") + else: + self.domain = list() + def add_element(self, element): + self.domain.append(element) + + def export(self): + self.domain = list(map(lambda x: repr(x) if isinstance(x, Expr) else x, self.domain)) + return StrVector(self.domain) + +class Param: + def __init__(self, domain: ParameterDomain, condition: Expr = True_(), switch = ""): + self.domain = domain + self.condition = condition + self.switch = switch + + def set_condition(self, condition: Expr): + self.condition = condition + +class Parameters: + def __init__(self): + pass + + def _export(self): + names = [] + types = [] + switches = [] + domain = [] + conditions = [] + for attr in dir(self): + if not re.match("^__.+__$", attr) and not re.match('^_export$', attr): + irace_assert(isinstance(getattr(self, attr), Param), f"The parameter has to be of type Param, but found {type(getattr(self, attr))}") + names.append(attr) + types.append(getattr(self, attr).domain.type.value) + switches.append(getattr(self, attr).switch) + domain.append(getattr(self, attr).domain.export()) + conditions.append(getattr(self, attr).condition.export()) + + names = StrVector(names) + types = StrVector(types) + switches = StrVector(switches) + domain = TaggedList(domain) + conditions = TaggedList(conditions) + return irace_pkg.readParametersData(names = names, types = types, switches = switches, domain = domain, conditions = conditions) + \ No newline at end of file diff --git a/tests/test_config_space.py b/tests/test_config_space.py new file mode 100644 index 0000000..9271d5e --- /dev/null +++ b/tests/test_config_space.py @@ -0,0 +1,175 @@ +from ConfigSpace.read_and_write import pcs +from rpy2.robjects.packages import importr +import io +from contextlib import redirect_stdout +from irace.compatibility.config_space import convert_from_config_space + +irace_pkg = importr('irace') + +pcss = ''' +barrier_algorithm {0, 1, 2, 3} [0] +barrier_crossover {-1, 0, 1, 2} [0] +barrier_limits_corrections {-1, 0, 1, 4, 16, 64} [-1] +barrier_limits_growth [1000000.0, 100000000000000.0] [1000000000000.0]l +barrier_ordering {0, 1, 2, 3} [0] +barrier_startalg {1, 2, 3, 4} [1] +emphasis_memory {no} [no] +emphasis_mip {0, 1, 2, 3, 4} [0] +emphasis_numerical {yes, no} [no] +feasopt_mode {0, 1, 2, 3, 4, 5} [0] +lpmethod {0, 1, 2, 3, 4, 5, 6} [0] +mip_cuts_cliques {-1, 0, 1, 2, 3} [0] +mip_cuts_covers {-1, 0, 1, 2, 3} [0] +mip_cuts_disjunctive {-1, 0, 1, 2, 3} [0] +mip_cuts_flowcovers {-1, 0, 1, 2} [0] +mip_cuts_gomory {-1, 0, 1, 2} [0] +mip_cuts_gubcovers {-1, 0, 1, 2} [0] +mip_cuts_implied {-1, 0, 1, 2} [0] +mip_cuts_mcfcut {-1, 0, 1, 2} [0] +mip_cuts_mircut {-1, 0, 1, 2} [0] +mip_cuts_pathcut {-1, 0, 1, 2} [0] +mip_cuts_zerohalfcut {-1, 0, 1, 2} [0] +mip_limits_aggforcut [0, 10] [3]i +mip_limits_cutpasses {-1, 0, 1, 4, 16, 64} [0] +mip_limits_cutsfactor [1.0, 16.0] [4.0]l +mip_limits_gomorycand [50, 800] [200]il +mip_limits_gomorypass {0, 1, 4, 16, 64} [0] +mip_limits_submipnodelim [125, 2000] [500]il +mip_ordertype {0, 1, 2, 3} [0] +mip_strategy_backtrack {0.9, 0.99, 0.999, 0.9999, 0.99999, 0.999999} [0.9999] +mip_strategy_bbinterval [1, 1000] [7]il +mip_strategy_branch {-1, 0, 1} [0] +mip_strategy_dive {0, 1, 2, 3} [0] +mip_strategy_file {0, 1} [1] +mip_strategy_fpheur {-1, 0, 1, 2} [0] +mip_strategy_heuristicfreq {-1, 0, 5, 10, 20, 40, 80} [0] +mip_strategy_lbheur {yes, no} [no] +mip_strategy_nodeselect {0, 1, 2, 3} [1] +mip_strategy_presolvenode {-1, 0, 1, 2} [0] +mip_strategy_probe {-1, 0, 1, 2, 3} [0] +mip_strategy_rinsheur {-1, 0, 5, 10, 20, 40, 80} [0] +mip_strategy_search {0, 1, 2} [0] +mip_strategy_startalgorithm {0, 1, 2, 3, 4, 5, 6} [0] +mip_strategy_subalgorithm {0, 1, 2, 3, 4, 5} [0] +mip_strategy_variableselect {-1, 0, 1, 2, 3, 4} [0] +network_netfind {1, 2, 3} [2] +network_pricing {0, 1, 2} [0] +preprocessing_aggregator {-1, 0, 1, 4, 16, 64} [-1] +preprocessing_boundstrength {-1, 0, 1} [-1] +preprocessing_coeffreduce {0, 1, 2} [2] +preprocessing_dependency {-1, 0, 1, 2, 3} [-1] +preprocessing_dual {-1, 0, 1} [0] +preprocessing_fill [2, 40] [10]il +preprocessing_linear {0, 1} [1] +preprocessing_numpass {-1, 0, 1, 4, 16, 64} [-1] +preprocessing_reduce {0, 1, 2, 3} [3] +preprocessing_relax {-1, 0, 1} [-1] +preprocessing_repeatpresolve {-1, 0, 1, 2, 3} [-1] +preprocessing_symmetry {-1, 0, 1, 2, 3, 4, 5} [-1] +read_scale {-1, 0, 1} [0] +sifting_algorithm {0, 1, 2, 3, 4} [0] +simplex_crash {-1, 0, 1} [1] +simplex_dgradient {0, 1, 2, 3, 4, 5} [0] +simplex_limits_perturbation {0, 1, 4, 16, 64} [0] +simplex_limits_singularity [2, 40] [10]il +simplex_perturbation_switch {no, yes} [no] +simplex_pgradient {-1, 0, 1, 2, 3, 4} [0] +simplex_pricing {0, 1, 4, 16, 64} [0] +simplex_refactor {0, 4, 16, 64, 256} [0] +simplex_tolerances_markowitz [0.0001, 0.5] [0.01]l +mip_limits_strongcand [2, 40] [10]il +mip_limits_strongit {0, 1, 4, 16, 64} [0] +mip_strategy_order {yes, no} [yes] +perturbation_constant [1e-08, 0.0001] [1e-06]l + +mip_strategy_order | mip_ordertype in {1, 2, 3} +mip_limits_strongcand | mip_strategy_variableselect in {3} +mip_limits_strongit | mip_strategy_variableselect in {3} +perturbation_constant | simplex_perturbation_switch in {yes} +''' + +cs = pcs.read(io.StringIO(pcss)) + +params = convert_from_config_space(cs) + +with io.StringIO() as buf, redirect_stdout(buf): + irace_pkg.printParameters(params._export()) + output = buf.getvalue() + +s = '''barrier_algorithm "" c (0,1,2,3) +barrier_crossover "" c (-1,0,1,2) +barrier_limits_corrections "" c (-1,0,1,4,16,64) +barrier_limits_growth "" r,log (1000000,100000000000000) +barrier_ordering "" c (0,1,2,3) +barrier_startalg "" c (1,2,3,4) +emphasis_memory "" c (no) +emphasis_mip "" c (0,1,2,3,4) +emphasis_numerical "" c (yes,no) +feasopt_mode "" c (0,1,2,3,4,5) +lpmethod "" c (0,1,2,3,4,5,6) +mip_cuts_cliques "" c (-1,0,1,2,3) +mip_cuts_covers "" c (-1,0,1,2,3) +mip_cuts_disjunctive "" c (-1,0,1,2,3) +mip_cuts_flowcovers "" c (-1,0,1,2) +mip_cuts_gomory "" c (-1,0,1,2) +mip_cuts_gubcovers "" c (-1,0,1,2) +mip_cuts_implied "" c (-1,0,1,2) +mip_cuts_mcfcut "" c (-1,0,1,2) +mip_cuts_mircut "" c (-1,0,1,2) +mip_cuts_pathcut "" c (-1,0,1,2) +mip_cuts_zerohalfcut "" c (-1,0,1,2) +mip_limits_aggforcut "" i (0,10) +mip_limits_cutpasses "" c (-1,0,1,4,16,64) +mip_limits_cutsfactor "" r,log (1,16) +mip_limits_gomorycand "" i,log (50,800) +mip_limits_gomorypass "" c (0,1,4,16,64) +mip_limits_strongcand "" i,log (2,40) | (mip_strategy_variableselect == "3") +mip_limits_strongit "" c (0,1,4,16,64) | (mip_strategy_variableselect == "3") +mip_limits_submipnodelim "" i,log (125,2000) +mip_ordertype "" c (0,1,2,3) +mip_strategy_backtrack "" c (0.9,0.99,0.999,0.9999,0.99999,0.999999) +mip_strategy_bbinterval "" i,log (1,1000) +mip_strategy_branch "" c (-1,0,1) +mip_strategy_dive "" c (0,1,2,3) +mip_strategy_file "" c (0,1) +mip_strategy_fpheur "" c (-1,0,1,2) +mip_strategy_heuristicfreq "" c (-1,0,5,10,20,40,80) +mip_strategy_lbheur "" c (yes,no) +mip_strategy_nodeselect "" c (0,1,2,3) +mip_strategy_order "" c (yes,no) | (mip_ordertype %in% c("1", "2", "3")) +mip_strategy_presolvenode "" c (-1,0,1,2) +mip_strategy_probe "" c (-1,0,1,2,3) +mip_strategy_rinsheur "" c (-1,0,5,10,20,40,80) +mip_strategy_search "" c (0,1,2) +mip_strategy_startalgorithm "" c (0,1,2,3,4,5,6) +mip_strategy_subalgorithm "" c (0,1,2,3,4,5) +mip_strategy_variableselect "" c (-1,0,1,2,3,4) +network_netfind "" c (1,2,3) +network_pricing "" c (0,1,2) +perturbation_constant "" r,log (0.00000001,0.0001) | (simplex_perturbation_switch == "yes") +preprocessing_aggregator "" c (-1,0,1,4,16,64) +preprocessing_boundstrength "" c (-1,0,1) +preprocessing_coeffreduce "" c (0,1,2) +preprocessing_dependency "" c (-1,0,1,2,3) +preprocessing_dual "" c (-1,0,1) +preprocessing_fill "" i,log (2,40) +preprocessing_linear "" c (0,1) +preprocessing_numpass "" c (-1,0,1,4,16,64) +preprocessing_reduce "" c (0,1,2,3) +preprocessing_relax "" c (-1,0,1) +preprocessing_repeatpresolve "" c (-1,0,1,2,3) +preprocessing_symmetry "" c (-1,0,1,2,3,4,5) +read_scale "" c (-1,0,1) +sifting_algorithm "" c (0,1,2,3,4) +simplex_crash "" c (-1,0,1) +simplex_dgradient "" c (0,1,2,3,4,5) +simplex_limits_perturbation "" c (0,1,4,16,64) +simplex_limits_singularity "" i,log (2,40) +simplex_perturbation_switch "" c (no,yes) +simplex_pgradient "" c (-1,0,1,2,3,4) +simplex_pricing "" c (0,1,4,16,64) +simplex_refactor "" c (0,4,16,64,256) +simplex_tolerances_markowitz "" r,log (0.0001,0.5) +''' + +assert output == s \ No newline at end of file diff --git a/tests/test_read_parameters.py b/tests/test_read_parameters.py new file mode 100644 index 0000000..037be0b --- /dev/null +++ b/tests/test_read_parameters.py @@ -0,0 +1,75 @@ +import pytest +from irace import Symbol, Parameters, Param, Integer, Real, Categorical, Ordinal, Min, In, List + +from rpy2.robjects.packages import importr +from irace.expressions import dputpy +base = importr('base') +irace_pkg = importr('irace') + + +def test_1(): + parameters = Parameters() + + parameters.algorithm = Param(Categorical(('as', 'mmas', 'eas', 'ras', 'acs')), switch="--") + + + s = ''' + # name switch type values [conditions (using R syntax)] + algorithm "--" c (as,mmas,eas,ras,acs) + ''' + + a = parameters._export() + b = irace_pkg.readParameters(text = s) + + assert base.identical(a, b)[0] + +def test_2(): + parameters = Parameters() + + parameters.alpha = Param(Real(0, 5), switch="--alpha ") + + s = ''' + # name switch type values [conditions (using R syntax)] + alpha "--alpha " r (0.00, 5.00) + ''' + + a = parameters._export() + b = irace_pkg.readParameters(text = s) + + assert base.identical(a, b)[0] + +def test_3(): + parameters = Parameters() + + values = Categorical(('0', '1', '2')) + values.add_element('3') + parameters.localsearch = Param(values, switch="--localsearch ") + + s = ''' + # name switch type values [conditions (using R syntax)] + localsearch "--localsearch " c (0, 1, 2, 3) + ''' + + a = parameters._export() + b = irace_pkg.readParameters(text = s) + print(dputpy(a)[0]) + print(dputpy(b)[0]) + + assert base.identical(a, b)[0] + +def test_4(): + with pytest.raises(Exception): + values = Categorical(('0', '1', '2', '2', '3', '4')) + +def test_5(): + with pytest.raises(Exception): + values = Categorical(('0', 1, 3)) + +def test_6(): + values = Categorical(("1", Symbol('abc') + 2)) + values.export() + +def test_7(): + with pytest.raises(Exception): + values = Categorical((1, 3, 4.0)) +