# --------------------------------------------------------------------------
# Source file provided under Apache License, Version 2.0, January 2004,
# http://www.apache.org/licenses/
# (c) Copyright IBM Corp. 2015, 2022
# --------------------------------------------------------------------------
from io import StringIO
from docplex.mp.utils import is_int, is_string
from docplex.mp.error_handler import docplex_fatal, DOcplexException
[docs]class ParameterGroup(object):
""" A group of parameters.
Note:
This class is not meant to be instantiated by users. Models come
with a full hierarchy of parameters with groups as nodes.
"""
def __init__(self, name, parent_group=None):
self._name = name
self._parent = parent_group
self._params = []
self._subgroups = []
if parent_group:
parent_group._add_subgroup(self)
def to_string(self, include_root=True):
return "group<%s>" % self.qualified_name(include_root=include_root)
def __str__(self):
return self.to_string()
def __repr__(self):
return "docplex.mp.params.ParameterGroup({0})".format(self.qualified_name())
[docs] def clone(self):
"""
Returns:
A deep copy of the parameter group.
"""
from copy import deepcopy
return deepcopy(self)
def copy(self):
return self.clone()
@property
def name(self):
""" This property returns the name of the group.
Note:
Parameter group names are always lowercase.
"""
return self._name
[docs] def iter_params(self):
""" Iterates over the group's own parameters.
Returns:
A iterator over directparameters belonging to the group, not including
sub-groups.
"""
return iter(self._params)
@property
def number_of_params(self):
""" This property returns the number of parameters in the group, not including subgroups.
"""
return len(self._params)
[docs] def total_number_of_params(self):
"""
Includes all parameters of subgroups recursively.
Returns:
integer: The total number of parameters inside the group.
"""
subparams = sum(g.total_number_of_params() for g in self._subgroups)
return subparams + self.number_of_params
def total_number_of_groups(self):
subgroups = sum(g.total_number_of_groups() for g in self._subgroups)
return subgroups + 1
@property
def number_of_subgroups(self):
""" This property returns the number of subgroups of the group, non-recursively.
"""
return len(self._subgroups)
def has_subgroups(self):
return len(self._subgroups) > 0
def iter_subgroups(self):
return iter(self._subgroups)
@property
def parent_group(self):
""" This property returns the parent group (an instance of :class:`ParameterGroup`), or None for the root group.
"""
return self._parent
def _add_param(self, param):
# internal
self._params.append(param)
def _add_subgroup(self, subgroup):
# internal
self._subgroups.append(subgroup)
[docs] def is_root(self):
""" Checks whether the group is the root group, in other words, has no parent group.
Returns:
Boolean: True if the group is the root group.
"""
return self._parent is None
def root_group(self):
group = self
while not group.is_root():
group = group.parent_group
return group
[docs] def qualified_name(self, sep=".", include_root=True):
""" Computes a string with all the parents of the parameters.
Example:
`parameter mip.mip_cuts.Cover` returns "mip.mip_cuts.Covers".
Args:
sep (string): The separator string. Default is ".".
include_root (flag): True if the root name is included.
Returns:
string: A string representation of the parameter hierarchy.
"""
self_parent = self._parent
if not self_parent:
return self.name
if not include_root and self_parent.is_root():
return self.name
else:
return "".join([self._parent.qualified_name(sep=sep, include_root=include_root), sep, self.name])
def prettyprint(self, indent=0):
tab = indent * 4 * " "
print("{0}{1!s}={{".format(tab, self.qualified_name(include_root=False)))
for p in self.iter_params():
print("{0} {1!s}".format(tab, p))
for sg in self.iter_subgroups():
assert isinstance(sg, ParameterGroup)
sg.prettyprint(indent + 1)
print("{0}}}".format(tab))
def _update_self_dict(self, extra_dict, do_check=True):
self_dict = self.__dict__
if do_check:
# new entries should not already be present in self.dict
for k in extra_dict:
if k in self_dict:
# should not happen
print("!! update_self_dict: name collision with: %s" % k) # pragma: no cover
self_dict.update(extra_dict)
def _restore_dict_recursive(self):
# internal use for deepcopy
param_dict = {p._name: p for p in self.iter_params()}
subgroup_dict = {sg._name: sg for sg in self.iter_subgroups()}
# update the __dict__ itself
self._update_self_dict(param_dict, do_check=False)
self._update_self_dict(subgroup_dict, do_check=False)
# recurse
for sg in self.iter_subgroups():
sg._restore_dict_recursive()
@staticmethod
def make(name, param_dict_fn, subgroup_fn, parent=None):
# INTERNAL
# factory method to create one group from:
# 1. a lambda function taking a group as argument and returning a dict of name: param instances
# 2. a dict of subgroup name: subgroup_make functions
# 3. a possibly-None parent group. If None, we are at root.
group = ParameterGroup(name, parent) if parent else RootParameterGroup(name, cplex_version=None)
group._initialize(param_dict_fn, subgroup_fn)
return group
def _initialize(self, param_dict_fn, subgroup_fn):
param_dict = param_dict_fn(self)
self._update_self_dict(param_dict)
if subgroup_fn:
subgroup_fn_dict = subgroup_fn()
subgroup_dict = {group_name: group_fn(self)
for group_name, group_fn in subgroup_fn_dict.items()}
self._update_self_dict(subgroup_dict)
def number_of_nondefaults(self):
return sum(1 for _ in self.generate_nondefault_params())
def has_nondefaults(self):
for _ in self.generate_nondefault_params():
return True
else:
return False
[docs] def reset(self, recursive=False):
""" Resets all parameters in the group.
Args:
recursive (Boolean): If True, also resets the subgroups.
"""
for p in self.iter_params():
p.reset()
if recursive:
for g in self.iter_subgroups():
g.reset(recursive=True)
def reset_all(self):
self.reset(recursive=True)
[docs] def generate_params(self):
""" A generator function that traverses all parameters.
The generator yields all parameters from the group
and also from its subgroups, recursively.
When called from the root parameter group, it returns all parameters.
Returns:
A generator object.
"""
return self._generate_and_filter_params(predicate=None)
def _generate_and_filter_params(self, predicate):
""" A filtering generator function that traverses a group's parameters.
This generator function traverses the group and its subgroup tree,
yielding only parameters that are accepted by th epredicate.
Args:
predicate: A function that takes one parameter as asrgument.
The return value of this function will be interpreted as a boolean using
Python conversion rules.
Returns:
A generator object.
"""
for p in self.iter_params():
if predicate is None or predicate(p):
yield p
# now recurse
for sg in self.iter_subgroups():
for nd in sg._generate_and_filter_params(predicate):
yield nd
[docs] def generate_nondefault_params(self):
""" A generator function that returns all non-default parameters.
This generator function traverses the group and its subgroup tree,
yielding those parameters with a non-default value, one at a time.
A parameter is non-default when its value differs from the default.
Returns:
A generator object.
"""
return self._generate_and_filter_params(predicate=lambda p: p.is_nondefault())
def generate_all_subgroups(self):
# INTERNAL
for sg in self.iter_subgroups():
yield sg
for ssg in sg.generate_all_subgroups():
yield ssg
def __setattr__(self, attr_name, value):
if attr_name.startswith("_"):
self.__dict__[attr_name] = value
elif hasattr(self, attr_name):
attr = getattr(self, attr_name)
if isinstance(attr, Parameter):
# attribute is set inside param, not necessarily in engine...
attr.set(value)
else:
docplex_fatal("No parameter with name {0} in {1}", attr_name, self.qualified_name())
else:
docplex_fatal("No parameter with name {0} in {1}", attr_name, self.qualified_name())
@property
def cplex_version(self):
return self.root_group().cplex_version
[docs]class Parameter(object):
""" Base class for all parameters.
This class is not meant to be instantiated but subclassed.
"""
__slots__ = ('_parent', '_name', '_cpx_name', '_id', '_description', '_default_value',
'_current_value', '_synchronous')
# This global flag controls checking new values.
# If set to False, assigned values are not checked for min/max ranges
skip_range_check = False
# noinspection PyProtectedMember
def __init__(self, group, short_name, cpx_name, param_key, description, default_value):
assert isinstance(group, ParameterGroup)
self._parent = group
self._name = short_name
self._cpx_name = cpx_name
self._id = param_key
self._description = description
self._default_value = default_value
# current = default at start...
self._current_value = default_value
self._synchronous = False
# link to parent group
group._add_param(self)
def ctor_name(self):
return self.__class__.__name__
def is_synchronous(self):
return self._synchronous
@property
def short_name(self):
return self._name
@property
def name(self):
return self._name
@property
def qualified_name(self):
""" Returns a hierarchical name string for the parameter.
The qualified name reflects the location of the parameter in the parameter hierarchy.
The qualified name of a parameter is guaranteed to be unique.
Examples:
`parameters.mip.tolerances.mipgap -> mip.tolerances.mipgap`
Returns:
A unique name that reflects the parameter location in the hierarchy.
:rtype:
string
"""
return self.get_qualified_name(sep='.', include_root=True)
def get_qualified_name(self, sep='.', include_root=True):
parent_qname = self._parent.qualified_name(sep=sep, include_root=include_root)
if parent_qname:
return "%s.%s" % (parent_qname, self.name)
else:
return self.name
@property
def cpx_name(self):
""" Returns the CPLEX name of the parameter. This string is the CPLEX reference name which is used
in CPLEX Reference Manual.
Examples:
`parameters.mip.tolerances.mipgap` has the name `CPX_PARAM_EPGAP` in CPLEX
:rtype:
string
"""
return self._cpx_name
@property
def cpx_id(self):
""" Returns the CPLEX integer code of the parameter. See the CPLEX Reference Manual for more
information.
Returns:
An integer code.
"""
return self._id
@property
def description(self):
""" Returns a string describing the parameter.
:rtype:
string
"""
return self._description
@property
def default_value(self):
""" Returns the default value of the parameter. This value can be numeric or a string,
depending on the parameter type.
Examples:
`parameters.workdir` has default value "."
`parameters.optimalitytarget` has default value 0
`parameters.mip.tolerances.mipgap` has default value of 0.0001
Returns:
The default value.
"""
return self._default_value
def reset_default_value(self, new_default):
# INTERNAL: use with caution
self._default_value = new_default # pragma: no cover
self._current_value = new_default # pragma: no cover
@property
def value(self):
return self._current_value
[docs] def accept_value(self, new_value):
""" Checks if `new_value` is an accepted value for the parameter.
Args:
new_value: The candidate value.
Returns:
Boolean: True if acceptable, else False.
"""
raise NotImplementedError() # pragma: no cover
def transform_value(self, raw_value):
# INTERNAL
return raw_value
def _check_value(self, raw_value):
if raw_value == self.default_value:
return raw_value
elif not self.accept_value(raw_value):
docplex_fatal("Value {0!r} of type {2} is invalid for parameter '{1}'",
raw_value, self.get_qualified_name(include_root=False), type(raw_value))
else:
return self.transform_value(raw_value)
def __call__(self, *args):
if not args:
return self._current_value
elif len(args) == 1:
self.set(args[0])
else:
docplex_fatal('Call parameter accepts either 0 or 1 argument')
[docs] def set(self, new_value):
""" Changes the value of the parameter to `new_value`.
This method checks that the new value has the proper type and is valid.
Numeric parameters often specify a valid range with a minimum and a maximum value.
If the value is valid, the current value of the parameter is changed, otherwise
an exception is raised.
Args:
new_value: The new value for the parameter.
Raises:
An exception if the value is not valid.
"""
accepted_value = self._check_value(new_value)
if accepted_value is not None:
self._current_value = accepted_value
if self._synchronous:
# print(" syncing {0!s} to {1}".format(self, accepted_value))
self.root_group().apply(self)
return accepted_value
[docs] def get(self):
""" Returns the current value of the parameter.
"""
return self._current_value
[docs] def reset(self):
""" Resets the parameter value to its default.
"""
self._current_value = self.default_value
[docs] def is_nondefault(self):
""" Checks if the current value of the parameter does not equal its default.
Returns:
Boolean: True if the current value of the parameter differs from its default.
"""
return self.get() != self._default_value
[docs] def is_default(self):
""" Checks if the current value of the parameter equals its default.
Returns:
Boolean: True if the current value of the parameter equals its default.
"""
return self.get() == self.default_value
@classmethod
def _is_in_range(cls, arg, range_min, range_max):
if not cls.skip_range_check:
if range_min is not None and arg < range_min:
return False
if range_max is not None and arg > range_max:
return False
return True
[docs] def to_string(self):
""" Converts the parameter to a string.
This method is used in the `__str__` method to convert a parameter to a string.
:rtype:
string
"""
return "{0}:{1:s}({2!s})".format(self._name, self.type_name, self._current_value)
def __str__(self):
return self.to_string()
def is_numeric(self):
# INTERNAL
return False # pragma: no cover
@property
def type_name(self):
# INTERNAL
raise NotImplementedError # pragma: no cover
def root_group(self):
return self._parent.root_group()
def _repr_classname(self):
return "docplex.mp.params.{0}".format(self.__class__.__name__)
def __repr__(self):
return "{0}({1},{2!s})".format(self._repr_classname(), self.qualified_name, self._current_value)
_BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True,
'0': False, 'no': False, 'false': False, 'off': False}
[docs]class BoolParameter(Parameter):
__slots__ = ()
def __init__(self, group, short_name, cpx_name, param_key, description, default_value):
Parameter.__init__(self, group, short_name, cpx_name, param_key, description, default_value)
def transform_value(self, new_value):
svalue = str(new_value).lower()
if new_value in {True, False}:
return new_value
elif new_value in {0, 1}:
return True if new_value else False
elif svalue in _BOOLEAN_STATES:
return _BOOLEAN_STATES[svalue]
else:
return None
[docs] def accept_value(self, value):
return value in {0, 1} or str(value).lower() in _BOOLEAN_STATES or value in {True, False}
@property
def type_name(self):
return "bool"
[docs]class StrParameter(Parameter):
__slots__ = ()
def __init__(self, group, short_name, cpx_name, param_key, description, default_value):
Parameter.__init__(self, group, short_name, cpx_name, param_key, description, default_value)
assert isinstance(default_value, str)
[docs] def accept_value(self, new_value):
return isinstance(new_value, str)
@property
def type_name(self):
return "string"
[docs] def to_string(self):
""" Converts the parameter to a string.
This method is used in the `__str__` method to convert a parameter to a string.
:rtype:
string
"""
safe_value = self._current_value or ''
return "{0}:{1:s}('{2!s}')".format(self._name, self.type_name, safe_value)
[docs]class IntParameter(Parameter):
__slots__ = ('_min_value', '_max_value')
[docs] def accept_value(self, new_value):
return is_int(new_value) and self._is_in_range(new_value, self._min_value, self._max_value)
def is_numeric(self):
return True # pragma: no cover
def _get_min_value(self):
return self._min_value # pragma: no cover
def _get_max_value(self):
return self._max_value # pragma: no cover
def __init__(self, group, short_name, cpx_name, param_key, description, default_value, min_value=None,
max_value=None, sync=False):
Parameter.__init__(self, group, short_name, cpx_name, param_key, description, default_value)
self._min_value = int(min_value)
self._max_value = int(max_value)
self._synchronous = sync
@property
def min_value(self):
return self._min_value
@property
def max_value(self):
return self._max_value
@property
def type_name(self):
return "int"
def __repr__(self):
return "{0}({1},{2!s})".format(self._repr_classname(), self.qualified_name, self._current_value)
def transform_value(self, new_value):
return int(new_value)
[docs]class PositiveIntParameter(IntParameter):
__slots__ = ()
def __init__(self, group, short_name, cpx_name, param_key, description, default_value, max_value=None):
IntParameter.__init__(self, group, short_name, cpx_name, param_key, description, default_value, min_value=0,
max_value=max_value)
@property
def type_name(self):
return "positive int"
[docs]class NumParameter(Parameter):
""" A numeric parameter can take any floating-point value in the range of `[min,max]` values.
"""
__slots__ = ('_min_value', '_max_value')
def __init__(self, group, short_name, cpx_name, param_key, description, default_value, min_value=None,
max_value=None):
Parameter.__init__(self, group, short_name, cpx_name, param_key, description, default_value)
self._min_value = min_value
self._max_value = max_value
def is_numeric(self):
return True # pragma: no cover
def _get_min_value(self):
return self._min_value # pragma: no cover
def _get_max_value(self):
return self._max_value # pragma: no cover
@property
def min_value(self):
return self._min_value
@property
def max_value(self):
return self._max_value
[docs] def accept_value(self, new_value):
fvalue = float(new_value)
return self._is_in_range(fvalue, self._min_value, self._max_value)
def transform_value(self, new_value):
return float(new_value)
@property
def type_name(self):
return "number"
# a dictionary of formats for each type.
_param_prm_formats = {NumParameter: "%.14f",
IntParameter: "%d",
PositiveIntParameter: "%d",
BoolParameter: "%d",
StrParameter: "\"%s\"" # need quotes
}
[docs]class RootParameterGroup(ParameterGroup):
""" The root parameter group (there should be only one instance at the root of the tree).
"""
def __init__(self, name, cplex_version):
ParameterGroup.__init__(self, name)
self._cplex_version = cplex_version
self._models = []
def __deepcopy__(self, memodict={}):
"""
Returns:
A deep copy of the parameter group.
"""
import copy
saved_models = self._models
self._models = []
new_root = RootParameterGroup(self.name, self._cplex_version)
memodict[id(self)] = new_root
for p in self.iter_params():
new_root._add_param(copy.copy(p))
for sg in self.iter_subgroups():
new_root._add_subgroup(copy.deepcopy(sg, memodict))
# add parameter and subgroup names to the inner __dict__ of the root
new_root._restore_dict_recursive()
# ---
for m in saved_models:
new_root.connect_model(m)
return new_root
def connect_model(self, m):
self._models.append(m)
def apply(self, param):
# apply one parameter to connected models
for m in self._models:
m.apply_one_parameter(param)
@property
def cplex_version(self):
return self._cplex_version
[docs] def is_root(self):
return True
[docs] def export_prm(self, out, overload_params=None):
"""
Exports parameters to an output stream in PRM format.
This method writes non-default parameters in CPLEX PRM syntax.
In addition to non-default parameters, some parameters can be forced to
be printed with a specific value by passing a dictionary with
Parameter objects as keys and values as arguments.
These values are used in the print operation, but will not be kept,
and the values of parameters will not be changed.
Passing `None` as `overload_params` will disable this functionality, and
only non-default parameters are printed.
Args:
out: The output stream, typically a filename.
overload_params: A dictionary of overloaded values for
certain parameters. This dictionary is of the form {param: value}
the printed PRM file will use overloaded values
for those parameters present in the dictionary.
"""
cplex_version_string = self._cplex_version
out.write("# -- This content is generated by DOcplex\n")
out.write("CPLEX Parameter File Version %s\n" % cplex_version_string)
param_generator = self.generate_params()
for param in param_generator:
if overload_params and param in overload_params:
param_value = overload_params[param]
else:
param_value = param.get()
if param_value != param.default_value:
out.write("{0:<33}".format(param.cpx_name))
out.write(_param_prm_formats[type(param)] % param_value)
out.write("\n")
out.write("# --- end of generated prm data ---\n")
def print_information(self, indent_level=0, print_all=False):
indent = ' ' * indent_level
param_generator = self.generate_params()
for param in param_generator:
if param.is_nondefault() or print_all:
param_value = param.get()
print("{0}{1} = {2!s}"
.format(indent,
param.qualified_name,
_param_prm_formats[type(param)] % param_value))
def export_prm_to_path(self, path, overload_params=None):
with open(path, mode='w') as out:
self.export_prm(out, overload_params=overload_params)
[docs] def print_info_to_stream(self, output, overload_params=None, print_defaults=False, indent_level=0):
""" Writes parameters to an output stream.
This method writes non-default parameters in a human readable syntax.
In addition to non-default parameters, some parameters can be forced to
be printed with a specific value by passing a dictionary with
Parameter objects as keys and values as arguments.
These values are used in the print operation but not be kept,
and the values of parameters will not be changed.
Passing `None` as `overload_params` will disable this functionality, and
only non-default parameters are printed.
Args:
output: The output stream.
overload_params: A dictionary of overloaded values for
certain parameters. This dictionary is of the form {param: value}.
"""
indent = " " * indent_level
param_generator = self.generate_params()
for param in param_generator:
if overload_params and param in overload_params:
param_value = overload_params[param]
elif print_defaults or param.is_nondefault():
param_value = param.get()
else:
param_value = None
if param_value is not None:
output.write("{0}{1} = {2!s}\n"
.format(indent,
param.qualified_name,
_param_prm_formats[type(param)] % param_value))
[docs] def export_prm_to_string(self, overload_params=None):
""" Exports non-default parameters in PRM format to a string.
The logic of overload is the same as in :func:`export_prm`.
A parameter is written if either it is a key on `overload_params`,
or it has a non-default value.
This allows merging non-default parameters with temporary parameter values.
Args:
overload_params: A dictionary of overloaded values, possibly None.
Note:
This method has no side effects on the parameters.
Returns:
string: A string in CPLEX PRM format.
"""
oss = StringIO()
self.export_prm(oss, overload_params)
return oss.getvalue()
def export_as_ops_file(self, filename):
with open(filename, mode='w') as out:
out.write(self.export_as_ops_file_to_string())
def export_as_ops_file_to_string(self):
from xml.etree.ElementTree import Element
from xml.etree import ElementTree
from xml.dom import minidom
def prettify(elem):
"""Return a pretty-printed XML string for the Element.
"""
rough_string = ElementTree.tostring(element=elem, encoding='utf-8')
reparsed = minidom.parseString(rough_string)
return reparsed.toprettyxml(indent=" ", )
settings = Element("settings")
settings.attrib = {"version": "2"}
category = Element("category")
category.attrib = {"name": "cplex"}
settings.append(category)
for p in self.generate_nondefault_params():
name = p.cpx_name.replace("CPX_PARAM_", "").lower()
if name == "startalg":
name = "rootalg"
elif name == "subalg":
name = "nodealg"
value = str(p.get())
if isinstance(p, BoolParameter):
if p.get() == 0:
value = "false"
else:
value = "true"
child = Element("setting")
child.attrib = {"name": name, "value": value}
category.append(child)
return prettify(settings)
[docs] def print_info_to_string(self, overload_params=None, print_defaults=False):
""" Writes parameters in readable format to a string.
The logic of overload is the same as in :func:`export_prm`.
A parameter is written if either it is a key on `overload_params`,
or it has a non-default value.
This allows merging non-default params with temporary parameter values.
Args:
overload_params: A dictionary of overloaded values, possibly None.
Note:
This method has no side effects on the parameters.
Returns:
A string.
"""
oss = StringIO()
self.print_info_to_stream(oss, print_defaults=print_defaults, overload_params=overload_params)
return oss.getvalue()
@staticmethod
def make(name, param_dict_fn, subgroup_fn, cplex_version):
# INTERNAL
# factory method to create one group from:
# 1. a lambda function taking a group as argument and returning a dict of name: param instances
# 2. a dict of subgroup name: subgroup_make functions
# 3. a possibly-None parent group. If None, we are at root.
root_group = RootParameterGroup(name, cplex_version)
root_group._initialize(param_dict_fn, subgroup_fn)
return root_group
def prettyprint(self, indent=0):
print("* CPLEX parameters version: {0}".format(self.cplex_version))
ParameterGroup.prettyprint(self, indent)
def __repr__(self):
return "docplex.mp.params.RootParameterGroup(%s)" % self.cplex_version
[docs] def qualified_name(self, sep='.', include_root=True):
return 'parameters' if include_root else ''
def as_dict(self):
# INTERNAL: returns a dictionary of qualified name -> parameter
qdict = {p.qualified_name: p for p in self}
return qdict
def __iter__(self):
for p in self.iter_params():
yield p
# now recurse
for sg in self.iter_subgroups():
for nd in sg._generate_and_filter_params(predicate=None):
yield nd
def find_parameter(self, key):
if is_int(key):
pred = lambda p_: p_.cpx_id == key
elif is_string(key):
# eliminate initial '.'
pred = lambda p_: p_.get_qualified_name(include_root=False) == key
else:
docplex_fatal('Parameters.find() accepts either integer code or path-like name, got: {0!r}'.format(key))
for p in self:
if pred(p):
return p
else:
return None
def set_from_qualified_name(self, qname, pvalue):
qname_list = qname.split('.')
if not qname_list:
# empty qname
return
groups = qname_list[:-1]
pname = qname_list[-1]
group = self
for g in groups:
group = getattr(group, g)
if group is None:
raise ValueError("Bad parameter group name: {0}".format(g))
res = None
try:
# extract parameter from group
target_param = getattr(group, pname)
if target_param:
target_param.set(pvalue)
res = pvalue
except AttributeError:
raise ValueError("Cannot find paramater {0} in group {1}".format(pname, group))
except DOcplexException as dex:
raise
return res