# --------------------------------------------------------------------------
# Source file provided under Apache License, Version 2.0, January 2004,
# http://www.apache.org/licenses/
# (c) Copyright IBM Corp. 2015, 2018
# --------------------------------------------------------------------------
# pylint: disable=too-many-lines
import os
import sys
import warnings
from itertools import chain, product
from io import StringIO
from docplex.mp.aggregator import ModelAggregator
from docplex.mp.constants import SOSType, CplexScope, ObjectiveSense, BasisStatus, EffortLevel,\
int_probtype_to_string, ComparisonType
from docplex.mp.constr import AbstractConstraint, LinearConstraint, RangeConstraint, \
IndicatorConstraint, QuadraticConstraint, PwlConstraint, EquivalenceConstraint
from docplex.mp.context import Context, OverridenOutputContext
from docplex.mp.dvar import is_var
from docplex.mp.engine_factory import EngineFactory
from docplex.mp.environment import Environment
from docplex.mp.error_handler import DefaultErrorHandler, \
docplex_add_trivial_infeasible_ct_here, handle_error
from docplex.mp.format import parse_format
from docplex.mp.lp_printer import LPModelPrinter
from docplex.mp.mfactory import ModelFactory
from docplex.mp.model_stats import ModelStatistics
from docplex.mp.numutils import round_nearest_towards_infinity1, _NumPrinter, compute_tolerance
from docplex.mp.pwl import PwlFunction
from docplex.mp.tck import get_typechecker, warn_trivial_feasible, warn_trivial_infeasible, warn_trivial_none
from docplex.mp.sttck import StaticTypeChecker
from docplex.mp.utils import DOcplexException, MultiObjective,\
DOcplexLimitsExceeded, _var_match_function
from docplex.mp.utils import is_indexable, is_iterable, is_int, is_string, \
make_output_path2, generate_constant, _AutomaticSymbolGenerator, _IndexScope, _to_list, \
is_number, str_maxed, normalize_basename, izip2_filled, ordered_sequence_to_list
from docplex.mp.utils import apply_thread_limitations
from docplex.mp.vartype import VarType, BinaryVarType, IntegerVarType, \
ContinuousVarType, SemiContinuousVarType, SemiIntegerVarType
from docplex.util.environment import get_environment
from docplex.mp.solve_env import CplexLocalSolveEnv
# noinspection PyProtectedMember
[docs]class Model(object):
""" This is the main class to embed modeling objects.
The :class:`Model` class acts as a factory to create optimization objects,
decision variables, and constraints.
It provides various accessors and iterators to the modeling objects.
It also manages solving operations and solution management.
The Model class is a context manager and can be used with the Python `with` statement:
.. code-block:: python
with Model() as mdl:
# start modeling...
When the `with` block is finished, the :func:`end` method is called automatically, and all resources
allocated by the model are destroyed.
If the model is not created with the `with` keyword, then the :func:`end` method should be called to free all CPLEX resources.
When a model is created without a specified ``context``, a default
``Context`` is created and initialized as described in :func:`docplex.mp.context.Context.read_settings`.
Example::
# Creates a model named 'my model' with default context
model = Model('my model')
In this example, we create a model to solve with just 2 threads::
context = Context.make_default_context()
context.cplex_parameters.threads = 2
model = Model(context=context)
Alternatively, this can be coded as::
model = Model()
model.context.cplex_parameters.threads = 2
Args:
name (optional): The name of the model.
context (optional): The solve context to be used. If no ``context`` is
passed, a default context is created.
log_output (optional): If ``True``, solver logs are output to
stdout. If this is a stream, solver logs are output to that
stream object.
checker (optional): If ``off``, then checking is disabled everywhere. Turning off checking
may improve performance but should be done only with extreme caution.
Possible values for the `checker` keyword argument are:
- `default` (or `std`, or `on`): detects modeling errors, but does not check
numerical values for infinities or NaNs. This is the default value.
- `numeric`: checks that numerical arguments are valid numbers, neither NaN nor
`math.infinity`. This option should be used when data are not trusted.
- `full`: performs all possible checks (This is the union of `std` and `numeric` checks).
- `off`: no typechecking is performed. This options must be used when the model has been
thoroughly tested and numerical data are trusted.
cts_by_name (optional): a flag which control whether the constraint name dictionary is enabled.
Default is False.
"""
_name_generator = _AutomaticSymbolGenerator(pattern="docplex_model", offset=1)
_default_effort_level = EffortLevel.Repair
@property
def binary_vartype(self):
""" This property returns an instance of :class:`docplex.mp.vartype.BinaryVarType`.
This type instance is used to build all binary decision variable collections of the model.
"""
return self._binary_vartype
@property
def integer_vartype(self):
""" This property returns an instance of :class:`docplex.mp.vartype.IntegerVarType`.
This type instance is used to build all integer variable collections of the model.
"""
return self._integer_vartype
@property
def continuous_vartype(self):
""" This property returns an instance of :class:`docplex.mp.vartype.ContinuousVarType`.
This type instance is used to build all continuous variable collections of the model.
"""
return self._continuous_vartype
@property
def semicontinuous_vartype(self):
""" This property returns an instance of :class:`docplex.mp.vartype.SemiContinuousVarType`.
This type instance is used to build all semi-continuous variable collections of the model.
"""
return self._semicontinuous_vartype
@property
def semiinteger_vartype(self):
""" This property returns an instance of :class:`docplex.mp.vartype.SemiIntegerType`.
This type instance is used to build all semi-integer variable collections of the model.
"""
return self._semiinteger_vartype
def _vartypes(self):
return (self._binary_vartype, self._integer_vartype, self._continuous_vartype,
self._semicontinuous_vartype, self._semiinteger_vartype)
def iter_vartypes(self):
return iter(self._vartypes())
def _parse_vartype(self, arg):
if isinstance(arg, VarType):
return arg
else:
self._checker.typecheck_string(arg, accept_empty=False, accept_none=False)
argl = arg.lower()
for vt in iter(self.iter_vartypes()):
if argl == vt.short_name.lower() or argl == vt.cplex_typecode.lower():
return vt
self.fatal("Cannot convert as a variable type: {0!r}", arg)
def _make_environment(self):
env = Environment.get_default_env()
# rtc-28869
env.numpy_hook = Model.init_numpy
return env
def _lazy_get_environment(self):
if self._environment is None:
self._environment = self._make_environment() # pragma: no cover
return self._environment
_saved_numpy_options = None
_unknown_status = None
[docs] @staticmethod
def init_numpy():
""" Static method to customize `numpy` for DOcplex.
This method makes `numpy` aware of DOcplex.
All `numpy` arrays with DOcplex objects will be printed by their string representations
as returned by `str(`) instead of `repr()` as with standard numpy behavior.
All customizations can be removed by calling the :func:`restore_numpy` method.
Note:
This method does nothing if `numpy` is not present.
See Also:
:func:`restore_numpy`
"""
try:
# noinspection PyUnresolvedReferences
import numpy as np
Model._saved_numpy_options = np.get_printoptions()
np.set_printoptions(formatter={'numpystr': Model._numpy_print_str, 'object': Model._numpy_print_str})
except ImportError: # pragma: no cover
pass # pragma: no cover
@staticmethod
def _numpy_print_str(arg):
return str(arg) if ModelFactory._is_operand(arg) else repr(arg)
[docs] @staticmethod
def restore_numpy(): # pragma: no cover
""" Static method to restore `numpy` to its default state.
This method is a companion method to :func:`init_numpy`. It restores `numpy` to its original state,
undoing all customizations that were done for DOcplex.
Note:
This method does nothing if numpy is not present.
See Also:
:func:`init_numpy`
"""
try:
# noinspection PyUnresolvedReferences
import numpy as np
if Model._saved_numpy_options is not None:
np.set_printoptions(Model._saved_numpy_options)
except ImportError: # pragma: no cover
pass # pragma: no cover
@property
def environment(self):
# for a closed model with no CPLEX, numpy, etc return ClosedEnvironment
# return get_no_graphics_env()
# from docplex.environment import ClosedEnvironment
# return ClosedEnvironment
return self._lazy_get_environment()
# ---- type checking
def _typecheck_var(self, obj):
self._checker.typecheck_var(obj)
def _typecheck_num(self, arg, caller=None):
self._checker.typecheck_num(arg, caller, safe_number=False)
def _typecheck_as_denominator(self, denominator, numerator):
StaticTypeChecker.typecheck_as_denominator(self, denominator, numerator)
def _typecheck_optional_num_seq(self, nums, accept_none=True, expected_size=None, caller=None):
return StaticTypeChecker.typecheck_optional_num_seq(self, nums, accept_none, expected_size, caller)
# ---
def unsupported_relational_operator_error(self, left_arg, op, right_arg):
# INTERNAL
self.fatal("Unsupported relational operator: {0!s} {1!s} {2!s}, only <=, ==, >= are allowed", left_arg, op,
right_arg)
def cannot_be_used_as_denominator_error(self, denominator, numerator):
StaticTypeChecker.cannot_be_used_as_denominator_error(self, denominator, numerator)
def unsupported_power_error(self, e, power):
self.fatal("Cannot raise {0!s} to the power {1}. A variable's exponent must be 0, 1 or 2.", e, power)
def _parse_kwargs(self, kwargs):
# parse some arguments from kwargs
for arg_name, arg_val in kwargs.items():
if arg_name == "float_precision":
self.float_precision = arg_val
elif arg_name in frozenset({'keep_ordering', 'ordering'}):
self._keep_ordering = bool(arg_val)
elif arg_name == "round_solution":
self._round_solution = bool(arg_val)
elif arg_name in frozenset({"info_level", "output_level"}):
self.output_level = arg_val
elif arg_name in {"agent", "solver_agent"}:
self.context.solver.agent = arg_val
elif arg_name == "log_output":
self.context.solver.log_output = arg_val
elif arg_name == "max_str_len":
self._max_str_len = int(arg_val)
elif arg_name == "use_space_str":
self._use_space_str = bool(arg_val)
elif arg_name == "keep_all_exprs":
self._keep_all_exprs = bool(arg_val)
elif arg_name == 'checker':
self._checker_key = arg_val.lower() if is_string(arg_val) else 'default'
elif arg_name == 'full_obj':
self._print_full_obj = bool(arg_val)
elif arg_name == 'lp_line_size':
self._lp_line_length = int(arg_val)
elif arg_name == 'ignore_names':
self._ignore_names = bool(arg_val)
elif arg_name == 'clean_before_solve':
self.clean_before_solve = arg_val
elif arg_name == 'quality_metrics':
self._quality_metrics = bool(arg_val)
elif arg_name in frozenset({'url', 'key'}):
# these two are known, no need to rant
pass
elif arg_name in frozenset({"parameters", 'cplex_parameters'}):
# update parameters either from a params object or a dict
self.context.update_cplex_parameters(arg_val)
elif arg_name == 'cts_by_name':
# safe
pass
else:
self.warning("keyword argument: {0:s}={1!s} - is not recognized (ignored)", arg_name, arg_val)
def _get_kwargs(self):
kwargs_map = {'float_precision': self.float_precision,
'keep_ordering': self.keep_ordering,
"round_solution": self._round_solution,
'output_level': self.output_level,
'solver_agent': self.solver_agent,
'log_output': self.log_output,
'max_str_len': self._max_str_len,
'use_space_str': self.str_use_space,
'keep_all_exprs': self._keep_all_exprs,
'checker': self._checker_key,
'full_obj': self._print_full_obj,
'lp_line_size': self._lp_line_length,
'ignore_names': self._ignore_names,
'clean_before_solve': self._clean_before_solve
}
return kwargs_map
def _new_engine(self, solver_agent):
return self._make_new_engine_from_agent(solver_agent)
@classmethod
def _new_linear_factory(cls, mdl, engine):
return ModelFactory(mdl, engine)
@classmethod
def _new_quadratic_factory(cls, mdl, engine):
from docplex.mp.quadfact import QuadFactory
return QuadFactory(mdl, engine)
def __init__(self, name=None, context=None, **kwargs):
"""Init a new Model.
Args:
name (optional): The name of the model
context (optional): The solve context to be used. If no ``context`` is
passed, a default context is created.
log_output (optional): if ``True``, solver logs are output to
stdout. If this is a stream, solver logs are output to that
stream object.
"""
if name is None:
name = Model._name_generator.new_symbol()
self._name = name
self._provenance = None
self._error_handler = DefaultErrorHandler(output_level='warning')
# type instances
self._binary_vartype = BinaryVarType()
self._integer_vartype = IntegerVarType()
self._continuous_vartype = ContinuousVarType()
self._semicontinuous_vartype = SemiContinuousVarType()
self._semiinteger_vartype = SemiIntegerVarType()
#
self._container_map = None
self._all_containers = []
self._origin_map = {}
self._vars_by_name = {}
self._cts_by_name = None
self.__allpwlfuncs = []
self._benders_annotations = None
self._constraint_priority_dict = {}
self._lazy_constraints = []
self._user_cuts = []
# -- kpis --
self._allkpis = []
self._progress_listeners = []
self._qprogress_listeners = []
self._mipstarts = []
# by default, ignore_names is off
self._ignore_names = False
# clean engine before solve (mip starts)
self._clean_before_solve = False # default is False: faster
# expression ordering
self._keep_ordering = False
# -- float formats
self._float_precision = 3
self._float_meta_format = '{%d:.3f}'
self._num_printer = _NumPrinter(self._float_precision)
self._environment = self._make_environment()
self_env = self._environment
# init context
if context is None:
self.context = Context.make_default_context(_env=self_env)
else:
self.context = context
# a flag to indicate whether ot not parameters have been version-checked.
self._synced_params = False
self._engine_factory = EngineFactory(env=self_env)
# maximum length for expression in str strings
self._max_str_len = 1e+10
self._readable_str_len = 48
# use spaces in expressions
self._use_space_str = False
# internal
self._keep_all_exprs = True # use False to get fast clone...with the risk of side effects...
# full objective lp
self._print_full_obj = False
# lp line size
self._lp_line_length = 80
# checker key
self._checker_key = 'default'
# quality_metrics
self._quality_metrics = False
# rond solution or not
self._round_solution = False
self._round_function = round_nearest_towards_infinity1
# update from kwargs, before the actual inits.
# pop cts_by name before parse kwargs
_enable_cts_by_name = kwargs.pop('cts_by_name', False)
# =======================================================
# parse without cts_by_name
self._parse_kwargs(kwargs)
self._cts_by_name = {} if _enable_cts_by_name else None
self._check_mip_for_mipstarts = True
self._checker = get_typechecker(arg=self._checker_key, logger=self.logger)
# -- scopes
self._var_scope = _IndexScope("var", cplex_scope=CplexScope.VAR_SCOPE)
self._linct_scope = _IndexScope("linear constraint",
cplex_scope=CplexScope.LINEAR_CT_SCOPE)
self._logical_scope = _IndexScope("logical constraint",
cplex_scope=CplexScope.IND_CT_SCOPE
)
self._quadct_scope = _IndexScope("quadratic constraint",
cplex_scope=CplexScope.QUAD_CT_SCOPE)
self._pwl_scope = _IndexScope("piecewise constraint",
cplex_scope=CplexScope.PWL_CT_SCOPE)
self._sos_scope = _IndexScope("SOS", cplex_scope=CplexScope.SOS_SCOPE)
self._scope_dict = {CplexScope.VAR_SCOPE: self._var_scope,
CplexScope.LINEAR_CT_SCOPE: self._linct_scope,
CplexScope.IND_CT_SCOPE: self._logical_scope,
CplexScope.QUAD_CT_SCOPE: self._quadct_scope,
CplexScope.PWL_CT_SCOPE: self._pwl_scope,
CplexScope.SOS_SCOPE: self._sos_scope
}
# init engine
engine = self._new_engine(self.solver_agent)
self.__engine = engine
# after engines
self._lfactory = self._new_linear_factory(self, engine)
self._qfactory = self._new_quadratic_factory(self, engine)
# after parse kwargs, after factories
self._aggregator = ModelAggregator(self._lfactory, self._qfactory)
self._quad_count = 0
self._solution = None
self._solve_details = None
self._last_solve_status = self._unknown_status
# all the following must be placed after an engine has been set.
self._objective_expr = None
self._multi_objective = MultiObjective.new_empty()
# engine log level
engineLogLevel = get_environment().get_parameter("oaas.engineLogLevel")
if engineLogLevel is not None and engineLogLevel in {"FINE", "FINER", "FINEST"}:
self.parameters.read.datacheck.set(2)
self.set_objective(sense=self.default_objective_sense,
expr=self._new_default_objective_expr())
model_hook_fn = self.context.model_build_hook
if model_hook_fn:
try:
model_hook_fn(self)
except Exception as me:
print("* Error in model_build_hook: {0!s}".format(me))
self._ctstatus_counter = 0
def _new_ct_status_index(self):
# INTERNAL
new_ct_status_index = self._ctstatus_counter + 1
self._ctstatus_counter = new_ct_status_index
return new_ct_status_index
@property
def name(self):
""" This property is used to get or set the model name.
"""
return self._name
def __repr__(self):
return self.to_string()
def to_string(self):
return "docplex.mp.Model['{0}']".format(self.name)
def __str__(self):
return self.to_string()
@property
def lfactory(self):
# INTERNAL
return self._lfactory
@name.setter
def name(self, name):
self._check_name(name)
self._name = name
def _check_name(self, new_name):
self._checker.typecheck_string(arg=new_name, accept_empty=False, accept_none=False)
if ' ' in new_name:
self.warning("Model name contains whitespaces: |{0:s}|", new_name)
@property
def provenance(self):
return self._provenance
def _get_obj_scope(self, cplex_scope, error='warn'):
# INTERNAL
ct_scope = self._scope_dict.get(cplex_scope)
if not ct_scope and error == 'raise':
raise ValueError("Unexpected scope code: {0}".format(cplex_scope))
return ct_scope
def _iter_scopes(self):
# INTERNAL
for _, scope in self._scope_dict.items():
yield scope
def _check_scope_indices(self):
for scope in self._iter_scopes():
scope.check_indices()
def _iter_constraint_scopes(self):
for cpxsc, scope in self._scope_dict.items():
if cpxsc.is_constraint_scope():
yield scope
def _sync_params(self, params):
# INTERNAL: execute only once
if self.has_cplex():
params.connect_model(self)
self_env = self._environment
self_cplex_parameters_version = self.context.cplex_parameters.cplex_version
self_engine = self.__engine
installed_cplex_version = self_env.cplex_version
# installed version is different from parameters: reset all defaults
if installed_cplex_version != self_cplex_parameters_version: # pragma: no cover
# cplex is more recent than parameters. must update defaults.
self.info(
"reset parameter defaults, from parameter version: {0} to installed version: {1}" # pragma: no cover
.format(self_cplex_parameters_version, installed_cplex_version)) # pragma: no cover
resets = self_engine._sync_parameter_defaults_from_cplex(params) # pragma: no cover
if resets:
for p, old, new in resets:
if p.name != 'randomseed': # usual practice to change randomseed at each version
self.info('parameter changed, name: {0}, old default: {1}, new default: {2}',
p.name, old, new)
@property
def infinity(self):
""" This property returns the numerical value used as the upper bound for continuous decision variables.
Note:
CPLEX usually sets this limit to 1e+20.
"""
return self.__engine.get_infinity()
[docs] def get_cplex(self, do_raise=True):
""" Returns the instance of Cplex used by the model, if any.
In case no local installation of CPLEX can be found, this method either raises an exception,
if parameter `do_raise` is True, or else returns None.
:param do_raise: An optional flag: if True, raise an exception when no Cplex instance
is available, otherwise return None.
See Also:
the 'cplex' property calls :func:`get_cplex()` with do_raise=True.
:return: an instance of Cplex, or None.
"""
return self._get_cplex(do_raise=do_raise)
def _get_cplex(self, do_raise=True, msgfn=None):
try:
cpx = self.__engine.get_cplex()
if cpx:
return cpx
except DOcplexException:
pass
if do_raise:
if msgfn:
raise_msg = msgfn()
else:
raise_msg = "CPLEX runtime not found - No instance of Cplex is available."
self.fatal(raise_msg)
else:
return None
@property
def cplex(self):
""" Returns the instance of Cplex used by the model, if any.
In case no local installation of CPLEX can be found, this method raises an exception.,
:return: a Cplex instance.
*New in version 2.15*
"""
return self.get_cplex(do_raise=True)
def has_cplex(self):
return self.get_cplex(do_raise=False) is not None
def _read_cplex_file(self, name, path, extension, cpx_read_fn):
# INTERNAL
cpx = self._get_cplex(do_raise=True, msgfn=lambda: "CPLEX runtime not found, cannot read CPLEX {0} file: {1}".format(name, path))
StaticTypeChecker.check_file(self, name=name, path=path, expected_extensions=(extension,))
cpx_read_fn(cpx, path)
@property
def cplex_matrix_stats(self):
cpx = self._get_cplex(do_raise=True, msgfn=lambda: "Model.cplex_matrix_stats requires cplex")
return cpx.get_stats()
[docs] def read_basis_file(self, bas_path):
""" Read a CPLEX basis status file.
This method requires the CPLEX runtime.
:param bas_path: the path of a basis file (extension is '.bas')
*New in version 2.10*
"""
self._read_cplex_file(name='basis', path=bas_path,
extension='.bas',
cpx_read_fn=lambda cpx_, path_: cpx_.start.read_basis(path_))
[docs] def read_priority_order_file(self, ord_path):
""" Read a CPLEX priority order file.
This method requires the CPLEX runtime.
:param ord_path: the path of a priority order file (extension is '.ord')
*New in version 2.10*
"""
self._read_cplex_file(name='priority order', path=ord_path,
extension='.ord',
cpx_read_fn=lambda cpx_, path_: cpx_.order.read(path_))
[docs] def export_priority_order_file(self, path=None, basename=None):
""" Exports a CPLEX priority order file.
This method requires the CPLEX runtime.
Args:
basename: Controls the basename with which the file is printed.
Accepts None, a plain string, or a string format.
If None, the model's name is used.
If passed a plain string, the string is used in place of the model's name.
path: A path to write the file, expects a string path or None.
Can be a directory, in which case the basename
that was computed with the basename argument, is appended to the directory to produce
the file.
If given a full path, the path is directly used to write the file, and
the basename argument is not used.
If passed None, the output directory will be ``tempfile.gettempdir()``.
Returns:
The full path of the written file, if successful,, else None.
*New in version 2.10*
"""
return self._write_cplex_file(name='priority order', path=path, basename=basename,
extension='.ord',
cpx_write_fn=lambda cpx_, path_: cpx_.order.write(path_))
@property
def str_max_len(self):
return self._max_str_len
@property
def readable_str_len(self):
return self._readable_str_len
@str_max_len.setter
def str_max_len(self, max_str):
assert max_str >= 1
self._max_str_len = max_str
@property
def str_use_space(self):
""" This boolean property controls the use of space separators when displaying the str()
representation of expressions (especially in constraints).
With `str_use_space=False` a constraint is printed as : `2x+3y+5z <= 7`
With `str_use_space=True` the same constraint is printed as : `2 x + 3 y + 5 z <= 7`
The default is False, that is print a compact representation.
:return: True if space separator is used for string representations of expressions.
"""
return self._use_space_str
@str_use_space.setter
def str_use_space(self, use_space):
self._use_space_str = bool(use_space)
@property
def str_space(self):
return ' ' if self._use_space_str else ''
@property
def keep_ordering(self):
return self._keep_ordering
@keep_ordering.setter
def keep_ordering(self, ordered): # pragma: no cover
# INTERNAL
b_ordered = bool(ordered)
self._keep_ordering = b_ordered
self._lfactory.update_ordering(b_ordered)
self._qfactory.update_ordering(b_ordered)
@property
def ignore_names(self):
""" This property is used to ignore all names in the model.
This flag indicates whether names are used or not.
When set to True, all names are ignored. This could lead to performance
improvements when building large models.
The default value of this flag is False. To change its value, add it as
keyword argument when creating the Model instance as in:
>>> m = Model(name="my_model", ignore_names=True)
Note:
Once a model instance has been created with `ignore_names=True`, there is no way to restore its names.
This flag only allows to enable or disable name generation while building the model.
"""
return self._ignore_names
@property
def float_precision(self):
""" This property is used to get or set the float precision of the model.
The float precision is an integer number of digits, used
in printing the solution and objective.
This number of digits is used for variables and expressions which are not discrete.
Discrete variables and objectives are printed with no decimal digits.
"""
return self._float_precision
@float_precision.setter
def float_precision(self, nb_digits):
used_digits = nb_digits
if nb_digits < 0:
self.warning("Negative float precision given: {0}, using 0 instead", nb_digits)
used_digits = 0
else:
max_digits = self.environment.max_nb_digits
bitness = self.environment.bitness
if nb_digits > max_digits:
self.warning("Given precision of {0:d} goes beyond {1:d}-bit capability, using maximum: {2:d}".
format(nb_digits, bitness, max_digits))
used_digits = max_digits
self._float_precision = used_digits
# recompute float format
self._float_meta_format = '{%%d:.%df}' % nb_digits
self._num_printer.precision = nb_digits
@property
def quality_metrics(self):
""" This flag controls whether CPLEX quality metrics are stored into the solve details.
The default is not to store quality metrics.
*New in version 2.10*
"""
return self._quality_metrics
@quality_metrics.setter
def quality_metrics(self, use_metrics):
self._quality_metrics = use_metrics
@property
def clean_before_solve(self):
return self._clean_before_solve
@clean_before_solve.setter
def clean_before_solve(self, must_clean):
self._clean_before_solve = bool(must_clean)
@property
def round_solution(self):
"""
This flag controls whether integer and discrete variable values are rounded in solutions, or not.
If not rounded, it may happen that solution value for a binary variable returns 0.99999.
The default is not to round discrete values.
*New in version 2.15*
"""
return self._round_solution
@round_solution.setter
def round_solution(self, do_round):
self._round_solution = bool(do_round)
def _round_element_value_if_necessary(self, elt, elt_value):
# INTERNAL
if self.round_solution and elt_value and elt.is_discrete() and elt_value != int(elt_value):
return self._round_function(elt_value)
else:
return elt_value
def has_cts_by_name_dict(self):
return self._cts_by_name is not None
def enable_cts_by_name_dict(self):
self._ensure_cts_name_dir()
def solved_stopped_by_limit(self):
sd = self.solve_details
return sd and sd.has_hit_limit()
@property
def time_limit(self):
""" This property is used to get/set the time limit for this model.
"""
return self.time_limit_parameter.get()
@time_limit.setter
def time_limit(self, new_time_limit):
self.set_time_limit(new_time_limit)
@property
def time_limit_parameter(self):
# INTERNAL
return self.parameters.timelimit
[docs] def get_time_limit(self):
"""
Returns:
The time limit for the model.
"""
return self.time_limit_parameter.get()
[docs] def set_time_limit(self, time_limit):
""" Set a time limit for solve operations.
Args:
time_limit: The new time limit; must be a positive number.
"""
self._checker.typecheck_num(time_limit)
if time_limit < 0:
self.fatal("Negative time limit: {0}", time_limit)
elif time_limit < 1:
self.warning("Time limit too small: {0} - using 1 instead", time_limit)
time_limit = 1
else:
pass
self.time_limit_parameter.set(time_limit)
@property
def lp_line_length(self):
""" This property lets you get or set the maximum line length of LP files generated by DOcplex.
The default is 80.
*New in version 2.11*
"""
return self._lp_line_length
@lp_line_length.setter
def lp_line_length(self, new_length):
if 70 <= new_length <= 512:
self._lp_line_length = new_length
else:
lpz = min(128, max(new_length, 70))
print(" LP line size set to: {0}, should be in [70..512], {1} was passed".format(lpz, new_length))
self._lp_line_length = lpz
@property
def solver_agent(self):
return self.context.solver.agent
def _set_solver_agent(self, new_agent):
assert new_agent
self.context.solver.agent = new_agent
@property
def error_handler(self):
return self._error_handler
@property
def logger(self):
return self._error_handler
@property
def solution(self):
""" This property returns the current solution of the model or None if the model has not yet been solved
or if the last solve has failed.
"""
return self._solution
def _get_solution(self):
# INTERNAL
return self._solution
def new_solution(self, var_value_dict=None, objective_value=None, name=None, **kwargs):
return self._lfactory.new_solution(var_value_dict=var_value_dict,
objective_value=objective_value, name=name, **kwargs)
[docs] def import_solution(self, source_solution, match="auto", error="raise"):
""" Imports a solution from another model.
There must a a way to map variables from the solution model to the target model,
either by name, index or some other custom manner.
The simplest case is where the other model is a clone of the target model.
In that case, an index-based mapping is used.
:param source_solution: the imported solution, built on some othe rmodel,
different from target model.
:param match: described the mapping used for variables, accepts either a string for
predefined mappings: "index" for index mapping, "name" for name mapping, or "auto" for
automatic. Also accepts a function taking two arguments: the source variable, and the target model,
returning th eimage of the source variable in the target model.
:param error: A string describing how errors are handled. Accepts "raise", "warn", or "ignore"
:return: A solution object, instance of :class:`SolveSolution`, built on the target model,
from values and variables mapped from the source model to the target model.
*New in version 2.21*
"""
target_model = self
find_matching_var = _var_match_function(source_model=source_solution.model,
target_model=target_model, match=match)
source_var_values = {}
source_keep_zeros = source_solution._keep_zeros
for dv, dvv in source_solution.iter_var_values():
target_var = find_matching_var(dv, target_model)
if target_var is None:
msg = "Cannot find matching variable in target model for {0!r}".format(dv)
handle_error(target_model, error, msg)
elif dvv or source_keep_zeros:
source_var_values[target_var] = dvv
source_obj = source_solution.objective_value
source_solved_by = source_solution.solved_by
newsol = target_model.new_solution(source_var_values, source_obj, keep_zeros=source_keep_zeros)
newsol._solved_by = source_solved_by
return newsol
[docs] def populate_solution_pool(self, **kwargs):
""" Populates and return a solution pool.
returns either a solutiion pool object, or None if the model solve fails.
This method accepts the same keyword arguments as :meth:`Model.solve`.
See the documentation of :meth:`Model.solve` for more details.
:return: an instance of :class:`docplex.mp.solution.SolutionPool`, or None.
See Also:
:class:`docplex.mp.solution.SolutionPool`.
*New in version 2.16*
"""
self_mname = 'Model.populate_solution_pool'
if not self.has_cplex():
self.fatal("{0} requires CPLEX, but a local CPLEX installation could not be found"
.format(self_mname))
pb_type = self._get_cplex_problem_type()
if pb_type not in {'MILP'}:
self.fatal("{0} only for MILP problems, model '{1}' is a {2}",
self_mname, self.name, pb_type)
elif self.has_multi_objective():
self.fatal("M{0}} is not available for multi-objective problems, model '{1}' has {2} objectives",
self_mname, self.name, self.number_of_multi_objective_exprs)
context = self.prepare_actual_context(**kwargs)
parameters = apply_thread_limitations(context)
raw_params = self.context._get_raw_cplex_parameters()
self_engine = self.__engine
sol = None
if raw_params and parameters is not raw_params:
saved_params = {p: p.get() for p in raw_params}
else:
saved_params = {}
log_stream = context.solver.log_output_as_stream
with OverridenOutputContext(self, log_stream):
used_clean_before_solve = kwargs.get('clean_before_solve', self.clean_before_solve)
try:
used_parameters = parameters or raw_params
# assert used_parameters is not None
self._apply_parameters_to_engine(used_parameters)
sol, solnpool = self.__engine.populate(clean_before_solve=used_clean_before_solve)
assert (sol is not None) == bool(solnpool)
finally:
solve_details = self_engine.get_solve_details()
self._notify_solve_hit_limit(solve_details)
self._solve_details = solve_details
self._set_solution(sol)
if saved_params:
for p, v in saved_params.items():
self_engine.set_parameter(p, v)
return solnpool
populate = populate_solution_pool
def restore_solution(self, sol, restore_all=True):
try:
if self != sol.model:
self.fatal("Model.restore_solution(): Expecting solution attached to model {0}, but attached to {1}"
.format(self.name, sol.model.name))
# check solution is linked to this model
sol.restore(self, restore_all=restore_all)
except AttributeError:
self.fatal("Model.restore_solution(): Expecting solution, {0!r} was passed", sol)
def fatal(self, msg, *args):
self._error_handler.fatal(msg, args)
def fatal_ce_limits(self, *args):
nb_vars = self.number_of_variables
nb_constraints = self.number_of_constraints
self._error_handler.fatal_limits_exceeded(nb_vars, nb_constraints)
def error(self, msg, *args):
self._error_handler.error(msg, args)
def warning(self, msg, *args):
self._error_handler.warning(msg, args)
def info(self, msg, *args):
self._error_handler.info(msg, args)
@property
def number_of_warnings(self):
return self._error_handler.number_of_warnings
@property
def number_of_errors(self):
return self._error_handler._number_of_errors
def trace(self, msg, *args):
self.logger.trace(msg, args)
@property
def output_level(self):
return self._error_handler.get_output_level()
@output_level.setter
def output_level(self, new_output_level):
self._error_handler.set_output_level(new_output_level)
def set_quiet(self):
self.logger.set_quiet()
def set_log_output(self, out=None):
self.context.solver.log_output = out
outs = self.context.solver.log_output_as_stream
self.__engine.set_streams(outs)
@property
def log_output(self):
return self.context.solver.log_output_as_stream
@log_output.setter
def log_output(self, out):
self.set_log_output(out)
def set_log_output_as_stream(self, outs):
self.__engine.set_streams(outs)
def is_logged(self):
return self.context.solver.log_output_as_stream is not None
def clear_engine_before_solve(self):
# INTERNAL
try:
self.__engine.clean_before_solve()
except AttributeError:
pass
[docs] def clear(self):
""" Clears the model of all modeling objects.
"""
self._clear_internal()
def _clear_internal(self, terminate=False):
self._container_map = None
self._all_containers = []
self._origin_map = {}
self._vars_by_name = {}
self._cts_by_name = None
self.__allpwlfuncs = []
self._benders_annotations = None
self._allkpis = []
self.clear_kpis()
self._last_solve_status = self._unknown_status
self._solution = None
self._mipstarts = []
self._clear_scopes()
self._lazy_constraints = []
self._user_cuts = []
self._quad_count = 0
if not terminate:
self.set_objective(sense=self.default_objective_sense,
expr=self._new_default_objective_expr())
self._set_engine(self._make_new_engine_from_agent(self.solver_agent, self.context))
else:
self._terminate_engine()
def _clear_scopes(self):
for a_scope in self._iter_scopes():
a_scope.clear()
def set_checker(self, checker_key):
# internal
if checker_key != self._checker_key:
new_checker = get_typechecker(arg=checker_key, logger=self.logger)
self._checker_key = checker_key
self._checker = new_checker
self._aggregator._checker = new_checker
self._lfactory._checker = new_checker
self._qfactory._checker = new_checker
def _make_new_engine_from_agent(self, solver_agent, context=None):
ctx = context or self.context
new_engine = self._engine_factory.new_engine(solver_agent, self.environment, model=self, context=ctx)
new_engine.set_streams(self.context.solver.log_output_as_stream)
return new_engine
def _set_engine(self, e2):
# INTERNAL
old_engine = self.get_engine()
self.__engine = e2
self._lfactory.update_engine(e2)
self._qfactory.update_engine(e2)
try:
self._static_terminate_engine(old_engine)
finally:
pass
def _terminate_engine(self):
# INTERNAL
old_engine = self.__engine
self._static_terminate_engine(old_engine)
self.__engine = None
@classmethod
def _static_terminate_engine(cls, engine_to_terminate):
if engine_to_terminate is not None:
# dispose of old engine.
engine_to_terminate.end()
# from Ryan
del engine_to_terminate
def _parse_agent(self, new_agent):
if not new_agent:
new_agent = self.context.solver.agent
elif not is_string(new_agent):
self.fatal('unexpected value for agent: {0!r} was passed, expecting string or None', new_agent)
return new_agent
def _set_new_engine_from_agent(self, agent_arg):
new_agent = self._parse_agent(agent_arg)
new_engine = self._make_new_engine_from_agent(new_agent, self.context)
self._set_engine(new_engine)
@property
def solves_with(self):
return self.__engine.name
def get_engine(self):
# INTERNAL for testing
return self.__engine
def _get_cplex_problem_type(self, fallback="unknown", do_raise=False):
# INTERNAL
cpx = self.get_cplex(do_raise=do_raise)
return self._problem_type_from_cplex(cpx, fallback)
def _problem_type_from_cplex(self, cpx, fallback=None):
# INTERNAL: decode cplex type code.
if cpx:
cpx_probtype_code = cpx.get_problem_type() # this is an int
return int_probtype_to_string(cpx_probtype_code)
else:
return fallback
@property
def problem_type(self):
""" Returns a string describing the type of problem.
This method requyires that CPLEX is installed and
available in PYTHONPATH. If the CPLEX runtime cannot be found, an exception is raised.
Possible values: LP, MILP, QP, MIQP, QCP, MIQCP,
*New in version 2.20*
"""
cpx = self._get_cplex(do_raise=True, msgfn=lambda: "Model.problem_type requites CPLEX")
return self._problem_type_from_cplex(cpx, fallback=None)
def __notify_new_model_object(self, descr,
mobj, mindex, mobj_name,
name_dir, idx_scope,
is_name_safe=False):
"""
Notifies the return af an object being create on the engine.
:param descr: A string describing the type of the object being created (e.g. Constraint, Variable).
:param mobj: The newly created modeling object.
:param mindex: The index as returned by the engine.
:param name_dir: The directory of objects by name (e.g. name -> constraint directory).
:param idx_scope: The index scope
"""
mobj._set_index(mindex)
if name_dir is not None:
mobj_name = mobj_name or mobj.name
if mobj_name:
# in some cases, names are checked before register
if not is_name_safe:
if mobj_name in name_dir:
old_name_value = name_dir[mobj_name]
# Duplicate constraint name: foo
self.warning("Duplicate {0} name: {1} already used for {2!r}", descr, mobj_name, old_name_value)
name_dir[mobj_name] = mobj
# store in idx dir if any
if idx_scope:
idx_scope.notify_obj_index(mobj, mindex)
def _register_one_var(self, var, var_index, var_name):
self.__notify_new_model_object("variable", var, var_index, var_name, self._vars_by_name, self._var_scope)
# @profile
def _register_block_vars(self, allvars, indices, allnames):
if allnames:
varname_dict = self._vars_by_name
for var, var_index, var_name in zip(allvars, indices, allnames):
var._index = var_index
if var_name:
if var_name in varname_dict:
old_name_value = varname_dict[var_name]
# Duplicate constraint name: foo
self.warning("Duplicate variable name: {0} already used for {1!s}", var_name, old_name_value)
varname_dict[var_name] = var
else:
for var, var_index in zip(allvars, indices):
var._index = var_index
self._var_scope.notify_obj_indices(objs=allvars, indices=indices)
def _register_one_constraint(self, ct, ct_index, is_ctname_safe=False):
"""
INTERNAL
:param ct: The new constraint to register.
:param ct_index: The index as returned by the engine.
:param is_ctname_safe: True if ct name has been checked for duplicates already.
:return:
"""
self.__notify_new_model_object(
"constraint", ct, ct_index, None,
self._cts_by_name, ct._get_index_scope(),
is_name_safe=is_ctname_safe)
def _ensure_cts_name_dir(self):
# INTERNAL: make sure the constraint name dir is present.
if self._cts_by_name is None:
self._cts_by_name = {ct.name: ct for ct in self.iter_constraints() if ct.has_user_name()}
return self._cts_by_name
def _register_block_cts(self, scope, cts, indices):
# INTERNAL: assert len(cts) == len(indices)
ct_name_map = self._cts_by_name
# --
if ct_name_map:
for ct, ct_index in zip(cts, indices):
ct._set_index(ct_index)
ct_name = ct.name
if ct_name:
ct_name_map[ct_name] = ct
else:
for ct, ct_index in zip(cts, indices):
ct._index = ct_index
scope.notify_obj_indices(cts, indices)
def _register_implicit_equivalence_ct(self, eqct, eqctx):
self._register_one_constraint(eqct, eqctx, is_ctname_safe=True)
# iterators
def _ensure_var_to_container_map(self):
# lazy build of var -> container mapping
ctn_map = self._container_map
if ctn_map is None:
ctn_map = {}
doomed_ctns = set()
for ctn in self.iter_var_containers():
try:
for dv in ctn:
ctn_map[dv] = ctn
except RuntimeError: # occurs when the dict has been modified
doomed_ctns.add(ctn)
self.warning(f"Modified variable container has been removed, name='{ctn.name}', shape={ctn.shape}")
self._container_map = ctn_map
# remove those var containers
if doomed_ctns:
#print(f"-- removing {len(doomed_ctns)} modified containers")
clean_containers = [c for c in self.iter_var_containers() if c not in doomed_ctns]
self._all_containers = clean_containers
return ctn_map
def iter_var_containers(self):
# INTERNAL
return iter(self._all_containers)
def _get_number_of_var_containers(self):
# INTERNAL
return len(self._all_containers)
def get_var_container(self, dvar):
# INTERNAL
ctn_map = self._ensure_var_to_container_map()
return ctn_map.get(dvar)
def set_var_container(self, dvar, ctn):
pass
def _add_var_container(self, var_container):
# INTERNAL
if var_container is not None:
self._all_containers.append(var_container)
ctn_map = self._container_map
if ctn_map is not None:
# increment var container map, if any
assert isinstance(ctn_map, dict)
for dv in var_container:
ctn_map[dv] = var_container
@staticmethod
def origin_key(obj):
# use id()here to allow case where index is not valid
return id(obj)
def get_obj_origin(self, obj):
# INTERNAL: retrieve origin of object
objkey = self.origin_key(obj)
return self._origin_map.get(objkey)
def set_obj_origin(self, obj, new_origin):
# INTERNAL: set origin of object
okey = self.origin_key(obj)
if new_origin is not None:
self._origin_map[okey] = new_origin
elif obj in self._origin_map:
del self._origin_map[okey]
def _is_binary_var(self, dvar):
return dvar.cplex_typecode == 'B'
def _is_integer_var(self, dvar):
return dvar.cplex_typecode == 'I'
def _is_continuous_var(self, dvar):
return dvar.cplex_typecode == 'C'
def _is_semicontinuous_var(self, dvar):
return dvar.cplex_typecode == 'S'
def _is_semiinteger_var(self, dvar):
return dvar.cplex_typecode == 'N'
@property
def number_of_generated_variables(self):
return sum(1 for v in self.iter_variables() if v.is_generated())
def _count_variables_w_code(self, cpxcode):
cnt = 0
for v in self.iter_variables():
if v.cplex_typecode == cpxcode:
cnt += 1
return cnt
def _iter_variables_w_code(self, cpxcode):
for v in self.iter_variables():
if v.cplex_typecode == cpxcode:
yield v
@property
def number_of_variables(self):
""" This property returns the total number of decision variables, all types combined.
"""
return self._var_scope.size
@property
def number_of_binary_variables(self):
""" This property returns the total number of binary decision variables added to the model.
"""
return self._count_variables_w_code('B')
@property
def number_of_integer_variables(self):
""" This property returns the total number of integer decision variables added to the model.
"""
return self._count_variables_w_code('I')
@property
def number_of_continuous_variables(self):
""" This property returns the total number of continuous decision variables added to the model.
"""
return self._count_variables_w_code('C')
@property
def number_of_semicontinuous_variables(self):
""" This property returns the total number of semi-continuous decision variables added to the model.
"""
return self._count_variables_w_code('S')
@property
def number_of_semiinteger_variables(self):
""" This property returns the total number of semi-integer decision variables added to the model.
"""
return self._count_variables_w_code('N')
@property
def number_of_user_variables(self):
return sum(1 for _ in self.generate_user_variables())
# def _has_discrete_var(self):
# # INTERNAL
# return any(v.is_discrete for v in self.iter_variables())
def _contains_discrete_artefacts(self):
if hasattr(self._lfactory, "_cached_justifier_discrete_var"):
return self._lfactory._cached_justifier_discrete_var is not None
elif self.number_of_sos or self._has_piecewise():
return True
for v in self.iter_variables():
if v.cplex_typecode in 'IBNS':
self._lfactory._cached_justifier_discrete_var = v
return True
return False
def _clear_cached_discrete_var(self):
lfactory = self._lfactory
if hasattr(lfactory, "_cached_justifier_discrete_var"):
lfactory._cached_justifier_discrete_var = None
def _has_piecewise(self):
return self._pwl_scope.size > 0
def _solved_as_mip(self):
# INTERNAL: is the model solved as mip (incl. engine status)
return self._contains_discrete_artefacts() or self.__engine.solved_as_mip()
def _solved_as_lp(self):
# INTERNAL: is the model solved as mip (incl. engine status)
if self.has_cplex():
return self.get_engine().solved_as_lp()
else:
return not self._contains_discrete_artefacts()
def is_quadratic(self):
# returns true if model is quadratic, that is
# either has atleast one quadratic constraint, or has a quadrtic objective.
return self._is_qc() or self._is_qp()
def _is_qc(self):
return self._quadct_scope.size > 0
def _is_qp(self):
if self.has_multi_objective():
return any(ex.is_quad_expr() for ex in self.iter_multi_objective_exprs())
else:
return self._objective_expr.is_quad_expr()
def _make_new_stats(self):
# INTERNAL
from collections import Counter
vartype_count = Counter(type(dv.vartype) for dv in self.iter_variables())
nbbvs = vartype_count[BinaryVarType]
nbivs = vartype_count[IntegerVarType]
nbcvs = vartype_count[ContinuousVarType]
nbscvs = vartype_count[SemiContinuousVarType]
nbsivs = vartype_count[SemiIntegerVarType]
linct_count = Counter(ct.cplex_code for ct in self.iter_binary_constraints())
nb_le_cts = linct_count['L']
nb_eq_cts = linct_count['E']
nb_ge_cts = linct_count['G']
nb_rng_cts= self.number_of_range_constraints
nb_ind_cts = self.number_of_indicator_constraints
nb_equiv_cts = self.number_of_equivalence_constraints
nb_quad_cts = self.number_of_quadratic_constraints
stats = ModelStatistics(nbbvs, nbivs, nbcvs, nbscvs, nbsivs,
nb_le_cts, nb_ge_cts, nb_eq_cts, nb_rng_cts,
nb_ind_cts, nb_equiv_cts, nb_quad_cts)
return stats
@property
def statistics(self):
""" Returns statistics on the model.
:returns: A new instance of :class:`docplex.mp.model_stats.ModelStatistics`.
"""
return self._make_new_stats()
def get_statistics(self):
return self.statistics
[docs] def iter_pwl_functions(self):
""" Iterates over all the piecewise linear functions in the model.
Returns the PWL functions in the order they were added to the model.
Returns:
An iterator object.
"""
return iter(self.__allpwlfuncs)
[docs] def iter_variables(self):
""" Iterates over all the variables in the model.
Returns the variables in the order they were added to the model,
regardless of their type.
Returns:
An iterator object.
"""
return self._var_scope.iter_objects()
[docs] def iter_binary_vars(self):
""" Iterates over all binary decision variables in the model.
Returns the variables in the order they were added to the model.
Returns:
An iterator object.
"""
return self._iter_variables_w_code('B')
[docs] def iter_integer_vars(self):
""" Iterates over all integer decision variables in the model.
Returns the variables in the order they were added to the model.
Returns:
An iterator object.
"""
return self._iter_variables_w_code('I')
[docs] def iter_continuous_vars(self):
""" Iterates over all continuous decision variables in the model.
Returns the variables in the order they were added to the model.
Returns:
An iterator object.
"""
return self._iter_variables_w_code('C')
[docs] def iter_semicontinuous_vars(self):
""" Iterates over all semi-continuous decision variables in the model.
Returns the variables in the order they were added to the model.
Returns:
An iterator object.
"""
return self._iter_variables_w_code('S')
[docs] def iter_semiinteger_vars(self):
""" Iterates over all semi-integer decision variables in the model.
Returns the variables in the order they were added to the model.
Returns:
An iterator object.
"""
return self._iter_variables_w_code('N')
[docs] def get_var_by_name(self, name):
""" Searches for a variable from a name.
Returns a variable if it finds one with exactly this name, or None.
Args:
name (str): The name of the variable being searched for.
:returns: A variable (instance of :class:`docplex.mp.dvar.Var`) or None.
"""
return self._vars_by_name.get(name, None)
def _disambiguate_varname(self, candidate_name):
# INTERNAL
varname_map = self._vars_by_name
if varname_map is None:
return candidate_name
safe_name = candidate_name
p = 1
while safe_name in varname_map:
safe_name = f"{candidate_name}#{p}"
p += 1
if p >= 1001:
raise DOcplexException(f"disambiguation fails for name: {candidate_name}")
return safe_name
def generate_user_variables(self):
# internal
for dv in self.iter_variables():
if not dv.is_generated():
yield dv
def generate_user_linear_constraints(self):
# internal
for lct in self.iter_linear_constraints():
if not lct.is_generated():
yield lct
[docs] def find_matching_vars(self, pattern, match_case=False):
""" Finds all variables whose name contain a given string
This method searches for all variables whose name
is not null and contains the passed ``pattern`` string. Anonymous variables
are not considered.
:param pattern: a non-empty string.
:param match_case: optional flag to match case (or not). Default is to not match case.
:return: A list of decision variables.
"""
return self._find_matching_objs(self.generate_user_variables, pattern, match_case, caller='Model.find_matching_vars')
[docs] def find_re_matching_vars(self, regexpr):
""" Finds all variables whose name match a regular expression.
This method searches for all variables with a name that
matches the given regular expression. Anonymous variables
are not counted as matching.
:param regexpr: a regular expression, as define din module ``re``
:return: A list of decision variables.
*New in version 2.9*
"""
matches = []
for dv in self.generate_user_variables():
dvname = dv.name
if dvname and regexpr.match(dvname):
matches.append(dv)
return matches
def _find_matching_objs(self, obj_iter, pattern, match_case=False, caller=None):
# internal
self._checker.typecheck_string(pattern, accept_empty=False, accept_none=False, caller=caller)
key_pattern = pattern if match_case else pattern.lower()
matches = []
for obj in obj_iter():
obj_name = obj.name
if obj_name:
matched = obj_name if match_case else obj_name.lower()
if key_pattern in matched:
matches.append(obj)
return matches
[docs] def find_matching_linear_constraints(self, pattern, match_case=False):
""" Finds all linear constraints whose name contain a given string
This method searches for all linear constraints whose name
is not empty and contains the passed ``pattern`` string. Anonymous linear constraints
are not considered.
:param pattern: a non-empty string.
:param match_case: optional flag to match case (or not). Default is to not match case.
:return: A list of linear constraints.
*New in version 2.9*
"""
return self._find_matching_objs(self.generate_user_linear_constraints, pattern, match_case,
caller='Model.find_matching_linear_constraints')
[docs] def find_matching_quadratic_constraints(self, pattern, match_case=False):
""" Finds all quadratic constraints whose name contain a given string
This method searches for all quadratic constraints whose name
is not empty and contains the passed ``pattern`` string. Anonymous constraints
are not considered.
:param pattern: a non-empty string.
:param match_case: optional flag to match case (or not). Default is to not match case.
:return: A list of quadratic constraints.
*New in version 2.23*
"""
return self._find_matching_objs(self.iter_quadratic_constraints, pattern, match_case,
caller='Model.find_matching_quadratic_constraints')
def get_var_by_index(self, idx):
# INTERNAL
return self._var_by_index(idx)
def _var_by_index(self, idx):
# INTERNAL
return self._var_scope.get_object_by_index(idx)
def _set_var_type(self, dvar, new_vartype_):
# INTERNAL
new_vartype = self._parse_vartype(new_vartype_)
if new_vartype != dvar.vartype:
self._change_var_types_internal((dvar,), (new_vartype,))
return dvar
def change_var_types(self, dvars, vartype_args):
parsefn = self._parse_vartype
checked_vars = list(self._checker.typecheck_var_seq(dvars, caller="Model.change_var_types"))
if is_iterable(vartype_args, accept_string=False):
new_vartypes = [parsefn(vt_arg) for vt_arg in vartype_args]
else:
new_vartype1 = parsefn(vartype_args)
new_vartypes = [new_vartype1] * len(checked_vars)
self._change_var_types_internal(checked_vars, new_vartypes)
def _change_var_types_internal(self, dvars, new_vartypes):
assert isinstance(dvars, (list, tuple))
# one batch call to engine
self.__engine.change_var_types(dvars, new_vartypes)
# update bounds, if necessary
for dv, nvt in zip(dvars, new_vartypes):
dv._set_vartype_internal(nvt)
self._update_var_bounds_from_type(dv, nvt)
self._clear_cached_discrete_var()
def set_var_name(self, dvar, new_name):
# INTERNAL: use var.name to set variable names
if new_name != dvar.name:
self.__engine.rename_var(dvar, new_name)
dvar._set_name(new_name)
def set_linear_constraint_name(self, linct, new_name):
# INTERNAL: use lct.name to set a linear constraint's name
if new_name != linct.name:
self.__engine.rename_linear_constraint(linct, new_name)
linct._set_name(new_name)
# ---- batch operations on variable bounds ----------------
[docs] def change_var_lower_bounds(self, dvars, lbs, check_bounds=True):
""" Changes lower bounds for a collection of variables in one call.
:param dvars: an iterable over decision variables (a list, or a comprehension)
:param lbs: accepts either an iterable over numbers, a single number,
in which case the new bound is applied to all variables,
or None. If passed None, the lower bound of each variable is reset to
its type's default.
:param check_bounds: an optional flag to enable or disable checking of
new lower bounds (default is True: check)
The logic for checking new lower bounds is as follows: new bounds are checked if and only if
`check` is True and the model checker is not 'off'.
Example:
>>> ivars = m.integer_var_list(3, lb=7)
>>> m.change_var_lower_bounds(ivars, [1,2,3]) # sets lower bounds to 1,2,3 resp.
*New in 2.20*
"""
checker = self._checker
var_list = checker.typecheck_var_seq(dvars, vtype=None, caller='Model.change_variable_lower_bounds')
if is_iterable(lbs):
lbs_ = (float(lb_) for lb_ in checker.typecheck_num_seq(lbs))
elif lbs is None:
lbs_ = (dv.vartype.default_lb for dv in dvars)
else:
checker.typecheck_num(lbs, caller='Model.change_variable_lower_bounds')
lbs_ = generate_constant(float(lbs), count_max=None)
def checked_lb(dvar, candidate_lb):
return dvar.vartype.resolve_lb(candidate_lb, self)
def to_float2(dvar, candidate_lb):
return candidate_lb
if check_bounds and checker.check_new_variable_bound():
# check bounds if not forced disable AND if checker allows it.
bound_transformer = checked_lb
else:
bound_transformer = to_float2
var_lb_dict = {dv: bound_transformer(dv, lb) for dv, lb in zip(var_list, lbs_)}
engine = self.get_engine()
engine.change_var_lbs(var_lb_dict)
# now update vars
for dv, lb2 in var_lb_dict.items():
dv._lb = lb2
[docs] def change_var_upper_bounds(self, dvars, ubs, check_bounds=True):
""" Changes upper bounds for a collection of variables in one call.
:param dvars: an iterable over decision variables (a list, or a comprehension)
:param ubs: accepts either an iterable over numbers, a single number,
in which case the new bound is applied to all variables,
or None. If passed None, the upper bound of each variable is reset to
its type's default.
:param check_bounds: an optional flag to enable or disable checking of
new upper bounds (default is True: check)
The logic for checking new upper bounds is as follows: new bounds are checked if and only if
`check` is True and the model checker is not 'off'.
Example:
>>> ivars = m.integer_var_list(3, lb=7)
>>> m.change_var_upper_bounds(ivars, [101,102,103]) # sets upper bounds to 1,01,102,103 resp.
*New in 2.20*
"""
checker = self._checker
var_list = checker.typecheck_var_seq(dvars, vtype=None, caller='Model.change_variable_upper_bounds')
if is_iterable(ubs):
ubs_ = (float(ub_) for ub_ in checker.typecheck_num_seq(ubs))
elif ubs is None:
ubs_ = (dv.vartype.default_ub for dv in dvars)
else:
checker.typecheck_num(ubs, caller='Model.change_variable_upper_bounds')
ubs_ = generate_constant(float(ubs), count_max=None)
def checked_ub(dvar, candidate_ub):
return dvar.vartype.resolve_ub(candidate_ub, self)
def to_float2(dvar, candidate_ub):
return candidate_ub
if check_bounds and checker.check_new_variable_bound():
# check bounds if not forced disable AND if checker allows it.
bound_transformer = checked_ub
else:
bound_transformer = to_float2
var_ub_dict = {dv: bound_transformer(dv, ub) for dv, ub in zip(var_list, ubs_)}
self.get_engine().change_var_ubs(var_ub_dict)
# now update vars
for dv, ub2 in var_ub_dict.items():
dv._ub = ub2
def set_var_lb(self, var, candidate_lb):
# INTERNAL: use var.lb to set lb
new_lb = var.vartype.resolve_lb(candidate_lb, self)
self._set_var_lb(var, new_lb)
return new_lb
def _set_var_lb(self, var, new_lb):
# INTERNAL
self.__engine.set_var_lb(var, new_lb)
var._internal_set_lb(new_lb)
def set_var_ub(self, var, candidate_ub):
# INTERNAL: use var.ub to set ub
new_ub = var.vartype.resolve_ub(candidate_ub, self)
self._set_var_ub(var, new_ub)
return new_ub
def _set_var_ub(self, var, new_ub):
# INTERNAL
self.__engine.set_var_ub(var, new_ub)
var._internal_set_ub(new_ub)
def _update_var_bounds_from_type(self, dvar, new_vartype, force_binary01=False):
# INTERNAL
old_lb, old_ub = dvar.lb, dvar.ub
if new_vartype == self.binary_vartype and force_binary01:
new_lb, new_ub = 0, 1
else:
new_lb = new_vartype.resolve_lb(old_lb, logger=self)
new_ub = new_vartype.resolve_ub(old_ub, logger=self)
if new_lb != old_lb:
self._set_var_lb(dvar, new_lb)
if new_ub != old_ub:
self._set_var_ub(dvar, new_ub)
[docs] def get_constraint_by_name(self, name):
""" Searches for a constraint from a name.
Returns the constraint if it finds a constraint with exactly this name, or None
if no constraint has this name.
This function will not raise an exception if the named constraint is not found.
Note:
The constraint name dicitonary in class Model is disabled by default. However,
calling `get_constraint_by_name` will compute one dicitonary on the fly,
but without warning for duplicate names. To enable the constraint
name dicitonary from the start (and get duplicate constraint messages),
add the `cts_by_name` keyword argument when creating the model, as in
>>> m = Model(name='my_model', cts_by_name=True)
This enables the constraint name dicitonary, and checks for duplicates when a named
constraint is added.
Args:
name (str): The name of the constraint being searched for.
Returns:
A constraint or None.
"""
return self._ensure_cts_name_dir().get(name)
[docs] def get_constraint_by_index(self, idx):
""" Searches for a linear constraint from an index.
Returns the linear constraint with `idx` as index, or None.
This function will not raise an exception if no constraint with this index is found.
Note: remember that linear constraints, logical constraints, and quadratic constraints
each have separate index spaces.
:param idx: a valid index (greater than 0).
:return: A linear constraint, or None.
"""
return self._linct_scope.get_object_by_index(idx, self._checker)
def get_logical_constraint_by_index(self, idx):
return self._logical_scope.get_object_by_index(idx, self._checker)
def get_pwl_constraint_by_index(self, idx):
return self._pwl_scope.get_object_by_index(idx, self._checker)
[docs] def get_quadratic_constraint_by_index(self, idx):
""" Searches for a quadratic constraint from an index.
Returns the quadratic constraint with `idx` as index, or None.
This function will not raise an exception if no constraint with this index is found.
Note: remember that linear constraints, logical constraints, and quadratic constraints
each have separate index spaces. Therefore, a model can contain both a linear constraint
and a quadratic constrait having index 0
:param idx: a valid index (greater than 0).
:return: A quadratic constraint, or None.
"""
return self._quadct_scope.get_object_by_index(idx, self._checker)
@property
def number_of_constraints(self):
""" This property returns the total number of constraints that were added to the model.
The number includes linear constraints, range constraints, and indicator constraints.
"""
return sum(scope.size for scope in self._iter_constraint_scopes())
@property
def number_of_user_constraints(self):
""" This property returns the total number of constraints that were
explicitly added tothe model, not including generated constraints.
The number includes all types of constraints.
"""
return sum(1 for ct in self.iter_constraints() if not ct.is_generated())
[docs] def iter_constraints(self):
""" Iterates over all constraints (linear, ranges, indicators).
Returns:
An iterator object over all constraints in the model.
"""
for sc in self._iter_constraint_scopes():
for obj in sc.iter_objects():
yield obj
def _count_constraints_with_type(self, scope, cttype):
return scope.count_filtered(pred=lambda ct: isinstance(ct, cttype))
@property
def number_of_range_constraints(self):
""" This property returns the total number of range constraints added to the model.
"""
return self._count_constraints_with_type(self._linct_scope, RangeConstraint)
@property
def number_of_linear_constraints(self):
""" This property returns the total number of linear constraints added to the model.
This counts binary linear constraints (<=, >=, or ==) and
range constraints.
See Also:
:func:`number_of_range_constraints`
"""
return self._linct_scope.size
[docs] def iter_range_constraints(self):
"""
Returns an iterator on the range constraints of the model.
Returns:
An iterator object.
"""
return self._linct_scope.generate_objects_with_type(RangeConstraint)
[docs] def iter_binary_constraints(self):
"""
Returns an iterator on the binary constraints (expr1 <op> expr2) of the model.
This does not include range constraints.
Returns:
An iterator object.
"""
return self._linct_scope.generate_objects_with_type(LinearConstraint)
[docs] def iter_linear_constraints(self):
"""
Returns an iterator on the linear constraints of the model.
This includes binary linear constraints and ranges but not indicators.
Returns:
An iterator object.
"""
for c in self.iter_constraints():
if c.is_linear():
yield c
@property
def number_of_nonzeros(self):
return sum(lct.size for lct in self.iter_linear_constraints())
[docs] def iter_indicator_constraints(self):
""" Returns an iterator on indicator constraints in the model.
Returns:
An iterator object.
"""
return self._logical_scope.generate_objects_with_type(IndicatorConstraint)
[docs] def iter_equivalence_constraints(self):
""" Returns an iterator on equivalence constraints in the model.
Returns:
An iterator object.
"""
return self._logical_scope.generate_objects_with_type(EquivalenceConstraint)
@property
def number_of_indicator_constraints(self):
""" This property returns the number of indicator constraints in the model.
"""
return self._count_constraints_with_type(self._logical_scope, IndicatorConstraint)
@property
def number_of_equivalence_constraints(self):
""" This property returns the number of equivalence constraints in the model.
"""
return self._count_constraints_with_type(self._logical_scope, EquivalenceConstraint)
[docs] def iter_quadratic_constraints(self):
"""
Returns an iterator on the quadratic constraints of the model.
Returns:
An iterator object.
"""
return self._quadct_scope.iter_objects()
@property
def number_of_quadratic_constraints(self):
""" This property returns the number of quadratic constraints in the model.
"""
return self._quadct_scope.size
def has_quadratic_constraint(self):
return self._quadct_scope.size > 0
def iter_logical_constraints(self):
return self._logical_scope.iter_objects()
[docs] def var(self, vartype, lb=None, ub=None, name=None):
""" Creates a decision variable and stores it in the model.
Args:
vartype: The type of the decision variable;
This field expects a concrete instance of the abstract class
:class:`docplex.mp.vartype.VarType`.
lb: The lower bound of the variable; either a number or None, to use the default.
The default lower bound for all three variable types is 0.
ub: The upper bound of the variable domain; expects either a number or None to use the type's default.
The default upper bound for Binary is 1, otherwise positive infinity.
name: An optional string to name the variable.
:returns: The newly created decision variable.
:rtype: :class:`docplex.mp.dvar.Var`
Note:
The model holds local instances of BinaryVarType, IntegerVarType, ContinuousVarType which
are accessible by properties (resp. binary_vartype, integer_vartype, continuous_vartype).
See Also:
:attr:`infinity`,
:attr:`binary_vartype`,
:attr:`integer_vartype`,
:attr:`continuous_vartype`
"""
self._checker.typecheck_vartype(vartype)
return self._var(vartype, lb, ub, name)
def _var(self, vartype, lb=None, ub=None, name=None):
# INTERNAL
if lb is not None:
self._checker.typecheck_num(lb, caller='Var.lb')
if ub is not None:
self._checker.typecheck_num(ub, caller='Var.ub')
return self._lfactory.new_var(vartype, lb, ub, name)
[docs] def continuous_var(self, lb=None, ub=None, name=None):
""" Creates a new continuous decision variable and stores it in the model.
Args:
lb: The lower bound of the variable, or None. The default is 0.
ub: The upper bound of the variable, or None, to use the default. The default is model infinity.
name (string): An optional name for the variable.
:returns: A decision variable with type :class:`docplex.mp.vartype.ContinuousVarType`.
:rtype: :class:`docplex.mp.dvar.Var`
"""
return self._var(self.continuous_vartype, lb, ub, name)
[docs] def integer_var(self, lb=None, ub=None, name=None):
""" Creates a new integer variable and stores it in the model.
Args:
lb: The lower bound of the variable, or None. The default is 0.
ub: The upper bound of the variable, or None, to use the default. The default is model infinity.
name: An optional name for the variable.
:returns: An instance of the :class:`docplex.mp.dvar.Var` class with type `IntegerVarType`.
:rtype: :class:`docplex.mp.dvar.Var`
"""
return self._var(self.integer_vartype, lb, ub, name)
[docs] def binary_var(self, name=None):
""" Creates a new binary decision variable and stores it in the model.
Args:
name (string): An optional name for the variable.
:returns: A decision variable with type :class:`docplex.mp.vartype.BinaryVarType`.
:rtype: :class:`docplex.mp.dvar.Var`
"""
return self._var(self.binary_vartype, name=name)
[docs] def semicontinuous_var(self, lb, ub=None, name=None):
""" Creates a new semi-continuous decision variable and stores it in the model.
Args:
lb: The lower bound of the variable (which must be strictly positive).
ub: The upper bound of the variable, or None, to use the default. The default is model infinity.
name (string): An optional name for the variable.
:returns: A decision variable with type :class:`docplex.mp.vartype.SemiContinuousVarType`.
:rtype: :class:`docplex.mp.dvar.Var`
"""
self._checker.typecheck_num(lb) # lb cannot be None
return self._var(self.semicontinuous_vartype, lb, ub, name)
[docs] def semiinteger_var(self, lb, ub=None, name=None):
""" Creates a new semi-integer decision variable and stores it in the model.
Args:
lb: The lower bound of the variable (which must be strictly positive).
ub: The upper bound of the variable, or None, to use the default. The default is model infinity.
name (string): An optional name for the variable.
:returns: A decision variable with type :class:`docplex.mp.vartype.SemiIntegerVarType`.
:rtype: :class:`docplex.mp.dvar.Var`
"""
self._checker.typecheck_num(lb) # lb cannot be None
return self._var(self.semiinteger_vartype, lb, ub, name)
def var_list(self, keys, vartype, lb=None, ub=None, name=str, key_format=None):
self._checker.typecheck_vartype(vartype)
return self._var_list(keys, vartype, lb, ub, name, key_format)
def _var_list(self, keys, vartype, lb=None, ub=None, name=str, key_format=None):
return self._lfactory.var_list(keys, vartype, lb, ub, name, key_format)
def var_dict(self, keys, vartype, lb=None, ub=None, name=str, key_format=None):
self._checker.typecheck_vartype(vartype)
return self._var_dict(keys, vartype, lb, ub, name, key_format)
def _var_dict(self, keys, vartype, lb=None, ub=None, name=str, key_format=None):
return self._lfactory.new_var_dict(keys, vartype, lb, ub, name, key_format, ordered=self._keep_ordering)
[docs] def binary_var_list(self, keys, lb=None, ub=None, name=str, key_format=None):
""" Creates a list of binary decision variables and stores them in the model.
Args:
keys: Either a sequence of objects or an integer.
name: Used to name variables. Accepts either a string or
a function. If given a string, the variable name is formed by appending the string
to the string representation of the key object (if keys is a sequence) or the
index of the variable within the range, if an integer argument is passed.
key_format: A format string or None. This format string describes how keys contribute to variable names.
The default is "_%s". For example if name is "x" and each key object is represented by a string
like "k1", "k2", ... then variables will be named "x_k1", "x_k2",...
Example:
If you want each key string to be surrounded by {}, use a special key_format: "_{%s}",
the %s denotes where the key string will be formatted and appended to `name`.
:returns: A list of :class:`docplex.mp.dvar.Var` objects with type :class:`doc.mp.vartype.BinaryVarType`.
Example:
`mdl.binary_var_list(3, "z")` returns a list of size 3
containing three binary decision variables with names `z_0`, `z_1`, `z_2`.
"""
return self._var_list(keys, self.binary_vartype, name=name, lb=lb, ub=ub, key_format=key_format)
[docs] def integer_var_list(self, keys, lb=None, ub=None, name=str, key_format=None):
""" Creates a list of integer decision variables with type `IntegerVarType`, stores them in the model,
and returns the list.
Args:
keys: Either a sequence of objects or a positive integer. If passed an integer,
it is interpreted as the number of variables to create.
lb: Lower bounds of the variables. Accepts either a floating-point number,
a list of numbers with the same size as keys,
a function (which will be called on each key argument), or None.
ub: Upper bounds of the variables. Accepts either a floating-point number,
a list of numbers with the same size as keys,
a function (which will be called on each key argument), or None.
name: Used to name variables. Accepts either a string or
a function. If given a string, the variable name is formed by appending the string
to the string representation of the key object (if `keys` is a sequence) or the
index of the variable within the range, if an integer argument is passed.
key_format: A format string or None. This format string describes how keys contribute to variable names.
The default is "_%s". For example if name is "x" and each key object is represented by a string
like "k1", "k2", ... then variables will be named "x_k1", "x_k2",...
Note:
Using None as the lower bound means the default lower bound (0) is used.
Using None as the upper bound means the default upper bound (the model's positive infinity)
is used.
:returns: A list of :class:`doc.mp.linear.Var` objects with type `IntegerVarType`.
Example:
>>> m.integer_var_list(3, name="ij")
returns a list of three integer variables from 0 to 1e+20, named ij_0, ij_1, ij_2.
The default behavior, when a string argument is passed as name,
is to concatenate the string , a "-" separator and a string representation
of the key; this allows to build arbitrary name strings from keys.
>>> m.integer_var_list(3, name=lambda i: "__name_{}__".format(i))
uses a functional name argument, producing names: "__name_0__",
"__name_1__", "_name__2__".
>>> m.integer_var_list(3, name="q", lb=1, ub=100)
returns a list of three integer variables from 1 to 100, named q_0, q_1, q_3
>>> m.integer_var_list([1,2,3], name="q", lb=lambda k: k, ub=lambda k: k*k+1)
returns a list of three integer variables q_1[1,1**1 +1], q2[2, 2*2+1], q3[3, 3*3+1]
The last example use the functional lb and ub, to compute bounds that
depend on the keys.
"""
return self._var_list(keys, self.integer_vartype, lb, ub, name, key_format)
[docs] def continuous_var_list(self, keys, lb=None, ub=None, name=str, key_format=None):
"""
Creates a list of continuous decision variables with type :class:`docplex.mp.vartype.ContinuousVarType`,
stores them in the model, and returns the list.
Args:
keys: Either a sequence of objects or a positive integer. If passed an integer,
it is interpreted as the number of variables to create.
lb: Lower bounds of the variables. Accepts either a floating-point number,
a list of numbers, a function, or None.
Use a number if all variables share the same lower bound.
Otherwise either use an explicit list of numbers
or use a function if lower bounds vary depending on the key, in which case,
the function will be called on each `key` in `keys`.
None means using the default lower bound (0) is used.
ub: Upper bounds of the variables. Accepts either a floating-point number,
a list of numbers, a function, or None.
Use a number if all variables share the same upper bound.
Otherwise either use an explicit list of numbers
or use a function if upper bounds vary depending on the key, in which case,
the function will be called on each `key` in `keys`.
None means the default upper bound (model infinity) is used.
name: Used to name variables. Accepts either a string or
a function. If given a string, the variable name is formed by appending the string
to the string representation of the key object (if `keys` is a sequence) or the
index of the variable within the range, if an integer argument is passed.
If passed a function, this function is called on each key object to generate a name.
The default behavior is to call :func:`str()` on each key object.
key_format: A format string or None. This format string describes how keys contribute to variable names.
The default is "_%s". For example if name is "x" and each key object is represented by a string
like "k1", "k2", ... then variables will be named "x_k1", "x_k2",...
Note:
When `keys` is either an empty list or the integer 0, an empty list is returned.
:returns: A list of :class:`docplex.mp.dvar.Var` objects with type :class:`docplex.mp.vartype.ContinuousVarType`.
See Also:
:attr:`infinity`
"""
return self._var_list(keys, self.continuous_vartype, lb, ub, name, key_format)
[docs] def semicontinuous_var_list(self, keys, lb, ub=None, name=str, key_format=None):
"""
Creates a list of semi-continuous decision variables with type :class:`docplex.mp.vartype.SemiContinuousVarType`,
stores them in the model, and returns the list.
Args:
keys: Either a sequence of objects or a positive integer. If passed an integer,
it is interpreted as the number of variables to create.
lb: Lower bounds of the variables. Accepts either a floating-point number,
a list of numbers, or a function.
Use a number if all variables share the same lower bound.
Otherwise either use an explicit list of numbers or
use a function if lower bounds vary depending on the key, in which case,
the function will be called on each `key` in `keys`.
Note that the lower bound of a semi-continuous variable must be strictly positive.
ub: Upper bounds of the variables. Accepts either a floating-point number,
a list of numbers, a function, or None.
Use a number if all variables share the same upper bound.
Otherwise either use an explicit list of numbers or
use a function if upper bounds vary depending on the key, in which case,
the function will be called on each `key` in `keys`.
None means the default upper bound (model infinity) is used.
name: Used to name variables. Accepts either a string or
a function. If given a string, the variable name is formed by appending the string
to the string representation of the key object (if `keys` is a sequence) or the
index of the variable within the range, if an integer argument is passed.
If passed a function, this function is called on each key object to generate a name.
The default behavior is to call :func:`str()` on each key object.
key_format: A format string or None. This format string describes how keys contribute to variable names.
The default is "_%s". For example if name is "x" and each key object is represented by a string
like "k1", "k2", ... then variables will be named "x_k1", "x_k2",...
Note:
When `keys` is either an empty list or the integer 0, an empty list is returned.
:returns: A list of :class:`docplex.mp.dvar.Var` objects with type :class:`docplex.mp.vartype.SemiContinuousVarType`.
See Also:
:attr:`infinity`
"""
return self._var_list(keys, self.semicontinuous_vartype, lb, ub, name, key_format)
[docs] def semiinteger_var_list(self, keys, lb, ub=None, name=str, key_format=None):
"""
Creates a list of semi-integer decision variables with type :class:`docplex.mp.vartype.SemiIntegerVarType`,
stores them in the model, and returns the list.
Args:
keys: Either a sequence of objects or a positive integer. If passed an integer,
it is interpreted as the number of variables to create.
lb: Lower bounds of the variables. Accepts either a floating-point number,
a list of numbers, or a function.
Use a number if all variables share the same lower bound.
Otherwise either use an explicit list of numbers or
use a function if lower bounds vary depending on the key, in which case,
the function will be called on each `key` in `keys`.
Note that the lower bound of a semi-integer variable must be strictly positive.
ub: Upper bounds of the variables. Accepts either a floating-point number,
a list of numbers, a function, or None.
Use a number if all variables share the same upper bound.
Otherwise either use an explicit list of numbers or
use a function if upper bounds vary depending on the key, in which case,
the function will be called on each `key` in `keys`.
None means the default upper bound (model infinity) is used.
name: Used to name variables. Accepts either a string or
a function. If given a string, the variable name is formed by appending the string
to the string representation of the key object (if `keys` is a sequence) or the
index of the variable within the range, if an integer argument is passed.
If passed a function, this function is called on each key object to generate a name.
The default behavior is to call :func:`str()` on each key object.
key_format: A format string or None. This format string describes how keys contribute to variable names.
The default is "_%s". For example if name is "x" and each key object is represented by a string
like "k1", "k2", ... then variables will be named "x_k1", "x_k2",...
Note:
When `keys` is either an empty list or the integer 0, an empty list is returned.
:returns: A list of :class:`docplex.mp.dvar.Var` objects with type :class:`docplex.mp.vartype.SemiIntegerVarType`.
See Also:
:attr:`infinity`
"""
return self._var_list(keys, self.semiinteger_vartype, lb, ub, name, key_format)
[docs] def continuous_var_dict(self, keys, lb=None, ub=None, name=None, key_format=None):
""" Creates a dictionary of continuous decision variables, indexed by key objects.
Creates a dictionary that allows retrieval of variables from business
model objects. Keys can be either a Python collection, an iterator, or a generator.
A key can be any Python object, with the exception of None.
Keys are used in the naming of variables.
Note:
If `keys` is empty, this method returns an empty dictionary.
The returned dictionary should not be modified.
Args:
keys: Either a sequence of objects, an iterator, or a positive integer. If passed an integer,
it is interpreted as the number of variables to create.
lb: Lower bounds of the variables. Accepts either a floating-point number,
a list of numbers, or a function.
Use a number if all variables share the same lower bound.
Otherwise either use an explicit list of numbers or
use a function if lower bounds vary depending on the key, in which case,
the function will be called on each `key` in `keys`.
None means the default lower bound (0) is used.
ub: Upper bounds of the variables. Accepts either a floating-point number,
a list of numbers, a function, or None.
Use a number if all variables share the same upper bound.
Otherwise either use an explicit list of numbers or
use a function if upper bounds vary depending on the key, in which case,
the function will be called on each `key` in `keys`.
None means the default upper bound (model infinity) is used.
name: Used to name variables. Accepts either a string or
a function. If given a string, the variable name is formed by appending the string
to the string representation of the key object (if `keys` is a sequence) or the
index of the variable within the range, if an integer argument is passed.
If passed a function, this function is called on each key object to generate a name.
The default behavior is to call :func:`str` on each key object.
key_format: A format string or None. This format string describes how `keys` contribute to variable names.
The default is "_%s". For example if name is "x" and each key object is represented by a string
like "k1", "k2", ... then variables will be named "x_k1", "x_k2",...
:returns: A dictionary of :class:`docplex.mp.dvar.Var` objects (with type `ContinuousVarType`) indexed by
the objects in `keys`.
See Also:
:class:`docplex.mp.dvar.Var`,
:attr:`infinity`
"""
return self._var_dict(keys, self.continuous_vartype, lb=lb, ub=ub, name=name, key_format=key_format)
[docs] def integer_var_dict(self, keys, lb=None, ub=None, name=None, key_format=None):
""" Creates a dictionary of integer decision variables, indexed by key objects.
Creates a dictionary that allows retrieval of variables from business
model objects. Keys can be either a Python collection, an iterator, or a generator.
A key can be any Python object, with the exception of None.
Keys are used in the naming of variables.
Note:
If `keys` is empty, this method returns an empty dictionary.
The returned dictionary should not be modified.
Args:
keys: Either a sequence of objects, an iterator, or a positive integer. If passed an integer,
it is interpreted as the number of variables to create.
lb: Lower bounds of the variables. Accepts either a floating-point number,
a list of numbers, or a function.
Use a number if all variables share the same lower bound.
Otherwise either use an explicit list of numbers or
use a function if lower bounds vary depending on the key, in which case,
the function will be called on each `key` in `keys`.
None means the default lower bound (0) is used.
ub: Upper bounds of the variables. Accepts either a floating-point number,
a list of numbers, a function, or None.
Use a number if all variables share the same upper bound.
Otherwise either use an explicit list of numbers or
use a function if upper bounds vary depending on the key, in which case,
the function will be called on each `key` in `keys`.
None means the default upper bound (model infinity) is used.
name: Used to name variables. Accepts either a string or
a function. If given a string, the variable name is formed by appending the string
to the string representation of the key object (if `keys` is a sequence) or the
index of the variable within the range, if an integer argument is passed.
If passed a function, this function is called on each key object to generate a name.
The default behavior is to call :func:`str` on each key object.
key_format: A format string or None. This format string describes how keys contribute to variable names.
The default is "_%s". For example if name is "x" and each key object is represented by a string
like "k1", "k2", ... then variables will be named "x_k1", "x_k2",...
:returns: A dictionary of :class:`docplex.mp.dvar.Var` objects (with type `IntegerVarType`) indexed by the
objects in `keys`.
See Also:
:attr:`infinity`
"""
return self._var_dict(keys, self.integer_vartype, lb=lb, ub=ub, name=name, key_format=key_format)
[docs] def binary_var_dict(self, keys, lb=None, ub=None, name=None, key_format=None):
""" Creates a dictionary of binary decision variables, indexed by key objects.
Creates a dictionary that allows retrieval of variables from business
model objects. Keys can be either a Python collection, an iterator, or a generator.
A key can be any Python object, with the exception of None.
Keys are used in the naming of variables.
Note:
If `keys` is empty, this method returns an empty dictionary.
The returned dictionary should not be modified.
Args:
keys: Either a sequence of objects, an iterator, or a positive integer. If passed an integer,
it is interpreted as the number of variables to create.
name (string): Used to name variables. Accepts either a string or
a function. If given a string, the variable name is formed by appending the string
to the string representation of the key object (if `keys` is a sequence) or the
index of the variable within the range, if an integer argument is passed.
key_format: A format string or None. This format string describes how keys contribute to variable names.
The default is "_%s". For example if name is "x" and each key object is represented by a string
like "k1", "k2", ... then variables will be named "x_k1", "x_k2",...
:returns: A dictionary of :class:`docplex.mp.dvar.Var` objects with type
:class:`docplex.mp.vartype.BinaryVarType` indexed by the objects in `keys`.
"""
return self._var_dict(keys, self.binary_vartype, lb=lb, ub=ub, name=name, key_format=key_format)
[docs] def semiinteger_var_dict(self, keys, lb, ub=None, name=str, key_format=None):
""" Creates a dictionary of semi-integer decision variables, indexed by key objects.
Creates a dictionary that allows retrieval of variables from business
model objects. Keys can be either a Python collection, an iterator, or a generator.
A key can be any Python object, with the exception of None.
Keys are used in the naming of variables.
Note:
If `keys` is empty, this method returns an empty dictionary.
The returned dictionary should not be modified.
Args:
keys: Either a sequence of objects, an iterator, or a positive integer. If passed an integer,
it is interpreted as the number of variables to create.
lb: Lower bounds of the variables. Accepts either a floating-point number,
a list of numbers, or a function.
Use a number if all variables share the same lower bound.
Otherwise either use an explicit list of numbers or
use a function if lower bounds vary depending on the key, in which case,
the function will be called on each `key` in `keys`.
ub: Upper bounds of the variables. Accepts either a floating-point number,
a list of numbers, a function, or None.
Use a number if all variables share the same upper bound.
Otherwise either use an explicit list of numbers or
use a function if upper bounds vary depending on the key, in which case,
the function will be called on each `key` in `keys`.
None means the default upper bound (model infinity) is used.
name: Used to name variables. Accepts either a string or
a function. If given a string, the variable name is formed by appending the string
to the string representation of the key object (if `keys` is a sequence) or the
index of the variable within the range, if an integer argument is passed.
If passed a function, this function is called on each key object to generate a name.
The default behavior is to call :func:`str` on each key object.
key_format: A format string or None. This format string describes how keys contribute to variable names.
The default is "_%s". For example if name is "x" and each key object is represented by a string
like "k1", "k2", ... then variables will be named "x_k1", "x_k2",...
:returns: A dictionary of :class:`docplex.mp.dvar.Var` objects (with type `SemiIntegerVarType`) indexed by the
objects in `keys`.
See Also:
:attr:`infinity`
"""
return self._var_dict(keys, self.semiinteger_vartype, lb, ub, name, key_format)
[docs] def semiinteger_var_matrix(self, keys1, keys2, lb, ub=None, name=None, key_format=None):
""" Creates a dictionary of semiinteger decision variables, indexed by pairs of key objects.
Creates a dictionary that allows the retrieval of variables from a tuple
of two keys, the first one from `keys1`, the second one from `keys2`.
In short, variables are indexed by the Cartesian product of the two key sets.
A key can be any Python object, with the exception of None.
Arguments `lb`, `ub`, `name`, and `key_format` are interpreted as in :func:`semiinteger_var_dict`.
*New in version 2.9*
"""
return self._var_multidict(self.semiinteger_vartype, [keys1, keys2], lb, ub, name, key_format)
[docs] def semicontinuous_var_dict(self, keys, lb, ub=None, name=str, key_format=None):
""" Creates a dictionary of semi-continuous decision variables, indexed by key objects.
Creates a dictionary that allows retrieval of variables from business
model objects. Keys can be either a Python collection, an iterator, or a generator.
A key can be any Python object, with the exception of None.
Keys are used in the naming of variables.
Note:
If `keys` is empty, this method returns an empty dictionary.
The returned dictionary should not be modified.
Args:
keys: Either a sequence of objects, an iterator, or a positive integer. If passed an integer,
it is interpreted as the number of variables to create.
lb: Lower bounds of the variables. Accepts either a floating-point number,
a list of numbers, or a function.
Use a number if all variables share the same lower bound.
Otherwise either use an explicit list of numbers or
use a function if lower bounds vary depending on the key, in which case,
the function will be called on each `key` in `keys`.
ub: Upper bounds of the variables. Accepts either a floating-point number,
a list of numbers, a function, or None.
Use a number if all variables share the same upper bound.
Otherwise either use an explicit list of numbers or
use a function if upper bounds vary depending on the key, in which case,
the function will be called on each `key` in `keys`.
None means the default upper bound (model infinity) is used.
name: Used to name variables. Accepts either a string or
a function. If given a string, the variable name is formed by appending the string
to the string representation of the key object (if `keys` is a sequence) or the
index of the variable within the range, if an integer argument is passed.
If passed a function, this function is called on each key object to generate a name.
The default behavior is to call :func:`str` on each key object.
key_format: A format string or None. This format string describes how keys contribute to variable names.
The default is "_%s". For example if name is "x" and each key object is represented by a string
like "k1", "k2", ... then variables will be named "x_k1", "x_k2",...
:returns: A dictionary of :class:`docplex.mp.dvar.Var` objects (with type `SemiIntegerVarType`) indexed by the
objects in `keys`.
See Also:
:attr:`infinity`
"""
return self._var_dict(keys, self.semicontinuous_vartype, lb, ub, name, key_format)
[docs] def semicontinuous_var_matrix(self, keys1, keys2, lb, ub=None, name=None, key_format=None):
""" Creates a dictionary of semicontinuous decision variables, indexed by pairs of key objects.
Creates a dictionary that allows the retrieval of variables from a tuple
of two keys, the first one from `keys1`, the second one from `keys2`.
In short, variables are indexed by the Cartesian product of the two key sets.
A key can be any Python object, with the exception of None.
Arguments `lb`, `ub`, `name`, and `key_format` are interpreted as in :func:`semiinteger_var_dict`.
*New in version 2.9*
"""
return self._var_multidict(self.semicontinuous_vartype, [keys1, keys2], lb, ub, name, key_format)
[docs] def var_hypercube(self, vartype_spec, seq_of_keys, lb=None, ub=None, name=None, key_format=None):
"""
Creates a dictionary of decision variables, indexed by tuples
of arbitrary size.
Arguments are analogous to methods of the type xxx_var_matrix,
except a type argument has to be passed.
Args:
vartype_spec: type specificsation: accepts either an instance of class
`docplex.mp.VarType`, or a string that can be translated into a vartype.
Possible strings are:
- cplex type codes, e.g. B,I,C,N,S or type short names
(e.g.: binary, integer, continuous, semicontinuous, semiinteger)
seq_of_keys: a sequence of sequence of keys. Typically of length >= 4,
as other dimensions are handled by the 'list', 'matrix' and 'cube'
series of methods.
Variables are indexed by tuples formed by the cartesian product of elements
form the sequences; all sequences of keys must be non-empty.
All other arguments have the same meaning as for all the "xx_var_matrix" family of methods.
Example:
>>> hc = Model().var_hypercube(vartype_spec='B', seq_of_keys=[[1,2], [3], ['a','b'], [1,2,3,4]]
>>> len(hc)
16
returns a dict of 2x2x4 = 16 variables indexed by tuples formed by the cartesian product
of the four lists, for example (1,3,'a',4)is a valid key for the hypercube.
*New in 2.19*
See Also:
:class:`docplex.mp.vartype.VarType`
"""
vartype = self._parse_vartype(vartype_spec)
self._checker.typecheck_vartype(vartype)
self._checker.typecheck_iterable(seq_of_keys)
lkeys = list(seq_of_keys)
arity = len(lkeys)
if arity == 0:
self.fatal("Variable hypercube with zero dimension")
return self._var_multidict(vartype, lkeys, lb, ub, name, key_format)
def _var_multidict(self, vartype, keys, lb=None, ub=None, name=None, key_format=None):
assert isinstance(vartype, VarType)
return self._lfactory.new_var_multidict(keys, vartype, lb, ub, name, key_format, ordered=self._keep_ordering)
def var_matrix(self, vartype, keys1, keys2, lb=None, ub=None, name=None, key_format=None):
return self._var_multidict(vartype, keys=[keys1, keys2],
lb=lb, ub=ub, name=name, key_format=key_format)
[docs] def binary_var_matrix(self, keys1, keys2, name=None, key_format=None):
""" Creates a dictionary of binary decision variables, indexed by pairs of key objects.
Creates a dictionary that allows the retrieval of variables from a tuple
of two keys, the first one from `keys1`, the second one from `keys2`.
In short, variables are indexed by the Cartesian product of the two key sets.
A key can be any Python object, with the exception of None.
Keys are used in the naming of variables.
Note:
If either of `keys1` or `keys2` is empty, this method returns an empty dictionary.
Args:
keys1: Either a sequence of objects, an iterator, or a positive integer. If passed an integer N,
it is interpreted as a range from 0 to N-1.
keys2: Same as `keys1`.
name: Used to name variables. Accepts either a string or
a function. If given a string, the variable name is formed by appending the string
to the string representation of the key object (if keys is a sequence) or the
index of the variable within the range, if an integer argument is passed.
key_format: A format string or None. This format string describes how keys contribute to variable names.
The default is "_%s". For example if name is "x" and each key object is represented by a string
like "k1", "k2", ... then variables will be named "x_k1", "x_k2",...
:returns: A dictionary of :class:`docplex.mp.dvar.Var` objects with type
:class:`docplex.mp.vartype.BinaryVarType` indexed by
all couples `(k1, k2)` with `k1` in `keys1` and `k2` in `keys2`.
"""
return self._var_multidict(self.binary_vartype, [keys1, keys2], 0, 1, name=name, key_format=key_format)
[docs] def integer_var_matrix(self, keys1, keys2, lb=None, ub=None, name=None, key_format=None):
""" Creates a dictionary of integer decision variables, indexed by pairs of key objects.
Creates a dictionary that allows the retrieval of variables from a tuple
of two keys, the first one from `keys1`, the second one from `keys2`.
In short, variables are indexed by the Cartesian product of the two key sets.
A key can be any Python object, with the exception of None.
Arguments `lb`, `ub`, `name`, and `key_format` are interpreted as in :func:`integer_var_dict`.
"""
return self._var_multidict(self.integer_vartype, [keys1, keys2], lb, ub, name, key_format)
[docs] def continuous_var_matrix(self, keys1, keys2, lb=None, ub=None, name=None, key_format=None):
""" Creates a dictionary of continuous decision variables, indexed by pairs of key objects.
Creates a dictionary that allows retrieval of variables from a tuple
of two keys, the first one from `keys1`, the second one from `keys2`.
In short, variables are indexed by the Cartesian product of the two key sets.
A key can be any Python object, with the exception of None.
Arguments `lb`, `ub`, `name`, and `key_format` are interpreted the same as in :func:`integer_var_dict`.
"""
return self._var_multidict(self.continuous_vartype, [keys1, keys2], lb, ub, name, key_format)
[docs] def continuous_var_cube(self, keys1, keys2, keys3, lb=None, ub=None, name=None, key_format=None):
""" Creates a dictionary of continuous decision variables, indexed by triplets of key objects.
Same as :func:`continuous_var_matrix`, except that variables are indexed by triplets of
the form `(k1, k2, k3)` with `k1` in `keys1`, `k2` in `keys2`, `k3` in `keys3`.
"""
return self._var_multidict(self.continuous_vartype, [keys1, keys2, keys3], lb, ub, name, key_format)
[docs] def integer_var_cube(self, keys1, keys2, keys3, lb=None, ub=None, name=str):
""" Creates a dictionary of integer decision variables, indexed by triplets.
Same as :func:`integer_var_matrix`, except that variables are indexed by triplets of
the form `(k1, k2, k3)` with `k1` in `keys1`, `k2` in `keys2`, `k3` in `keys3`.
See Also:
:func:`integer_var_matrix`
"""
return self._var_multidict(self.integer_vartype, [keys1, keys2, keys3], lb, ub, name)
[docs] def binary_var_cube(self, keys1, keys2, keys3, name=None, key_format=None):
"""Creates a dictionary of binary decision variables, indexed by triplets.
Same as :func:`binary_var_matrix`, except that variables are indexed by triplets of
the form `(k1, k2, k3)` with `k1` in `keys1`, `k2` in `keys2`, `k3` in `keys3`.
:returns: A dictionary of :class:`docplex.mp.dvar.Var` objects (with type :class:`docplex.mp.vartype.BinaryVarType`) indexed by
triplets.
"""
return self._var_multidict(self.binary_vartype, [keys1, keys2, keys3], name=name, key_format=key_format)
[docs] def linear_expr(self, arg=None, constant=0, name=None):
''' Returns a new empty linear expression.
Args:
arg: an optional argument to convert to a linear expression. Detailt is None,
in which case, an empty expression is returned.
:returns: An instance of :class:`docplex.mp.linear.LinearExpr`.
'''
self._checker.typecheck_num(arg=constant, caller='Model.linear_expr()')
if name:
warnings.warn("Naming expressions is deprecated -- ignored")
return self._lfactory.linear_expr(arg=arg, constant=constant)
[docs] def quad_expr(self, name=None):
''' Returns a new empty quadratic expression.
:returns: An empty instance of :class:`docplex.mp.quad.QuadExpr`.
'''
if name:
if self.is_docplex_debug():
raise RuntimeError
warnings.warn("Naming expressions is deprecated, use a variable if necessary")
return self._qfactory.new_quad()
[docs] def abs(self, e):
""" Builds an expression equal to the absolute value of its argument.
Args:
e: Accepts any object that can be transformed into an expression:
decision variables, expressions, or numbers.
Returns:
An expression that can be used in arithmetic operators and constraints.
Note:
Building the expression generates auxiliary decision variables, including binary decision variables,
and this may change the nature of the problem from a LP to a MIP.
"""
self._checker.typecheck_operand(e, caller="Model.abs", accept_numbers=True)
return self._lfactory.new_abs_expr(e)
[docs] def min(self, *args):
""" Builds an expression equal to the minimum value of its arguments.
This method accepts a variable number of arguments.
If no arguments are provided, returns positive infinity (see :attr:`infinity`).
Args:
args: A variable list of arguments, each of which is either an expression, a variable,
or a container.
If passed a container or an iterator, this container or iterator must be the unique argument.
If passed one dictionary, returns the minimum of the dictionary values.
Returns:
An expression that can be used in arithmetic operators and constraints.
Example:
`model.min()` -> returns `model.infinity`.
`model.min(e)` -> returns `e`.
`model.min(e1, e2, e3)` -> returns a new expression equal to the minimum of the values of `e1`, `e2`, `e3`.
`model.min([x1,x2,x3])` where `x1`, `x2` .. are variables or expressions -> returns the minimum of these expressions.
`model.min([])` -> returns `model.infinity`.
Note:
Building the expression generates auxiliary variables, including binary decision variables,
and this may change the nature of the problem from a LP to a MIP.
"""
min_args = args
nb_args = len(args)
if 0 == nb_args:
pass
elif 1 == nb_args:
unique_arg = args[0]
if is_iterable(unique_arg):
if isinstance(unique_arg, dict):
min_args = unique_arg.values()
else:
min_args = _to_list(unique_arg)
for a in min_args:
self._checker.typecheck_operand(a, caller="Model.min()")
else:
self._checker.typecheck_operand(unique_arg, caller="Model.min()")
else:
for arg in args:
self._checker.typecheck_operand(arg, caller="Model.min")
return self._lfactory.new_min_expr(*min_args)
[docs] def max(self, *args):
""" Builds an expression equal to the maximum value of its arguments.
This method accepts a variable number of arguments.
Args:
args: A variable list of arguments, each of which is either an expression, a variable,
or a container.
If passed a container or an iterator, this container or iterator must be the unique argument.
If passed one dictionary, returns the maximum of the dictionary values.
If no arguments are provided, returns negative infinity (see :attr:`infinity`).
Example:
`model.max()` -> returns `-model.infinity`.
`model.max(e)` -> returns `e`.
`model.max(e1, e2, e3)` -> returns a new expression equal to the maximum of the values of `e1`, `e2`, `e3`.
`model.max([x1,x2,x3])` where `x1`, `x2` .. are variables or expressions -> returns the maximum of these expressions.
`model.max([])` -> returns `-model.infinity`.
Note:
Building the expression generates auxiliary variables, including binary decision variables,
and this may change the nature of the problem from a LP to a MIP.
"""
max_args = args
nb_args = len(args)
if 0 == nb_args:
pass
elif 1 == nb_args:
unique_arg = args[0]
if is_iterable(unique_arg):
if isinstance(unique_arg, dict):
max_args = unique_arg.values()
else:
max_args = _to_list(unique_arg)
for a in max_args:
self._checker.typecheck_operand(a, caller="Model.max")
else:
self._checker.typecheck_operand(unique_arg, caller="Model.max")
else:
for arg in args:
self._checker.typecheck_operand(arg, caller="Model.max")
return self._lfactory.new_max_expr(*max_args)
[docs] def logical_and(self, *args):
""" Builds an expression equal to the logical AND value of its arguments.
This method takes a variable number of arguments, and accepts
binary variables, other logical expressions, or discrete constraints.
Args:
args: A variable list of logical operands.
Note:
If passed an empty number of arguments, this method an expression equal to 1.
Returns:
An expression, equal to 1 if and only if all of its
arguments are equal to 1, else equal to 0.
See Also:
:func:`logical_or`
:func:`logical_not`
Example::
# return logical XOR or two binary variables.
def logxor(m, b1, b2):
return m.logical_and(m.logical_or(b1, b2), m.logical_not(m.logical_and(b1, b2)))
"""
bvars = self._checker.typecheck_logical_op_seq(args, caller='Model.logical_and')
return self._lfactory.new_logical_and_expr(bvars)
[docs] def logical_or(self, *args):
""" Builds an expression equal to the logical OR value of its arguments.
This method takes a variable number of arguments, and accepts
binary variables, other logical expressions, or discrete constraints.
Args:
args: A variable list of logical operands.
Note:
If passed an empty number of arguments, this method a zero expression.
Returns:
An expression, equal to 1 if and only if at least one of its
arguments is equal to 1, else equal to 0.
See Also:
:func:`logical_and`
:func:`logical_not`
*New in version 2.11*
"""
bvars = self._checker.typecheck_logical_op_seq(args, caller='Model.logical_or')
return self._lfactory.new_logical_or_expr(bvars)
[docs] def logical_not(self, arg):
""" Builds an expression equal to the logical negation of its argument.
This method accepts either a binary variable, or another logical expression.
Args:
arg: A binary variable, or a logical expression,
e.g. an expression built by logical_and, logical_or, logical_not
Returns:
An expression, equal to 1 if its argument is 0, else 0.
See Also:
:func:`logical_and`
:func:`logical_or`
"""
StaticTypeChecker.typecheck_logical_op(self, arg, 'Model.logical_not')
return self._lfactory.new_logical_not_expr(arg)
[docs] def scal_prod(self, terms, coefs):
"""
Creates a linear expression equal to the scalar product of a sequence of decision variables
and a sequence of coefficients.
This method accepts different types of input for both arguments. `terms` can be any
iterable returning expressions or variables, and `coefs` is usually
an iterable returning numbers.
`cal_prod` also accept one number as `coefs`, in which case
the scalar product reduces to a sum times this coefficient.
:param terms: An iterable returning variables or expressions.
:param coefs: An iterable returning numbers, or a number.
Note:
- both iterables are iterated at the same time, so the order in which terms and numbers
are returned must be consistent: using unordered collections (e.g. sets) could lead to unexpected results.
- Iteration stops as soon as one iterable stops. If both iterables are empty, the method returns 0.
:return: A linear expression or 0.
"""
self._checker.check_ordered_sequence(arg=terms, caller='Model.scal_prod() requires a list of expressions/variables')
return self._aggregator.scal_prod(terms, coefs)
[docs] def dot(self, terms, coefs):
""" Synonym for :func:`scal_prod`.
"""
return self.scal_prod(terms, coefs)
[docs] def dotf(self, var_dict, coef_fn, assume_alldifferent=True):
""" Creates a scalar product from a dictionary of variables and a function.
This method is a functional variant of `dot`. I takes as asrgument a dictionary of variables,
as returned by xxx_var_dict or xx_var_var_matrix (where xxx is a type), and a function.
:param var_dict: a dictionary of variables, as returned by all the xxx_var_dict methods (e.g. integer_var_dict),
but also multi-dimensional dictionaries, such as xxx_var_matrix (or var_cube).
:param coef_fn: A function that takes one dictionary key and returns anumber. One-dimension dictionaries (such
as integer_var_dict) have plain object as keys, but multi-dimensional dictionaries have tuples keys.
For example, a binary_var_matrix returns a dictionary, the keys of which are 2-tuples.
:param assume_alldifferent: an optional flag whichi ndicates whether variables values in the dictionary
can be assumed to be all different. This is true when the dicitonary has been built by Docplex's
Model.xxx_var-dict(), and thi sis the default behavior.
For a custom-built dictionary, set the flag to False. A wrong flag value may yield incorrect results.
:return: an expression, built as a scalar product of all variables in the dictionay, multiplied by the result of the function.
Examples:
>>> m1 = m.binary_var_matrix(keys1=range(1,3), keys2=range(1,3), name='b')
>>> s = m.dotf(m1, lambda keys: keys[0] + keys[1])
returns 2 b_1_1 + 3 b_1_2 +3 b_2_1 + 4 b_2_2
"""
StaticTypeChecker.typecheck_callable\
(self, coef_fn,
"Functional scalar product requires a function taking variable keys as argument. A non-callable was passed: {0!r}".format(
coef_fn))
return self._aggregator._scal_prod_f(var_dict, coef_fn, assume_alldifferent)
scal_prod_f = dotf
[docs] def scal_prod_vars_all_different(self, terms, coefs):
""" Fastly creates a scalar product from a dictionary of variables and a list of coefficients or a coefficient.
:param terms: An iterable returning variables or expressions.
:param coefs: An iterable returning numbers, or a number.
"""
self._checker.check_ordered_sequence(arg=terms,
caller='Model.scal_prod() requires a list of expressions/variables')
var_seq = self._checker.typecheck_var_seq_all_different(terms)
return self._aggregator._scal_prod_vars_all_different(var_seq, coefs)
[docs] def sum(self, args):
""" Creates an expression summing over an iterable over expressions or variables.
This method expects an iterable over any type of expression: quadrayic expression,
linear expression, variables, constants.
Note:
The returned expression is quadratic as soon as the result is quadratic, otherwise it returns
a linear expression, or 0 if the iterable is empty.
:param args: An iterable over expressions (quadratic or linear), variables, or constants.
:return: A Docplex expression.
"""
return self._aggregator.sum(args)
[docs] def sums(self, *args):
""" Same as `Model.sum()` but accepts a variable number of arguments.
:param args: A variable number of expressions (quadratic or linear), variables, or constants.
:return: A Docplex expression.
Example:
Assuming x is a variable.
>>> m.sums(x**2, x, 1)
is identical to
>> m.sum([x**2, x, 1])
Both return a quadratic expression "x^2+x+1"
*New in version 2.22*
"""
return self.sum(args)
[docs] def sumsq(self, args):
""" Creates a quadratic expression summing squares of expressions.
Each element of the list is squared and added to the result. Quadratic expressions
are not accepted, as they cannot be squared.
Note:
This method returns 0 if the argument is an empty list or iterator.
:param args: An iterable returning linear expressions, variables or numbers.
:return: A quadratic expression (possibly constant).
"""
return self._aggregator.sumsq(args)
sum_squares = sumsq
[docs] def sum_vars(self, dvars):
""" Creates a linear expression that sums variables.
This method is faster than `Model.sum()` but accepts only variables.
:param dvars: an iterable returning variables.
:return: a linear expression equal to the sum of the variables.
*New in version 2.10*
"""
return self._aggregator._sum_vars(dvars)
[docs] def sum_vars_all_different(self, terms):
"""
Creates a linear expression equal to sum of a list of decision variables.
The variable sequence is a list or an iterator of variables.
This method is faster than the standard generic summation method due to the fact that it takes only
variables and does not take expressions as arguments.
:param terms: A list or an iterator on variables only, with no duplicates.
:return: a linear expression equal to the sum of the variables.
Note:
If the variable iteration contains duplicates, this function returns an incorrect result.
"""
if isinstance(terms, dict):
return self.sum_vars_all_different(terms.values())
else:
var_seq = self._checker.typecheck_var_seq_all_different(terms)
return self._aggregator._sum_vars_all_different(var_seq)
[docs] def le_constraint(self, lhs, rhs, name=None):
""" Creates a "less than or equal to" linear constraint.
Note:
This method returns a constraint object, that is not added to the model.
To add it to the model, use the :func:`add_constraint` method.
Args:
lhs: An object that can be converted to a linear expression, typically a variable,
a member of an expression.
rhs: An object that can be converted to a linear expression, typically a variable,
a member of an expression.
name (string): An optional string to name the constraint.
:returns: An instance of :class:`docplex.mp.linear.LinearConstraint`.
"""
return self._lfactory.new_le_constraint(lhs, rhs, name)
[docs] def ge_constraint(self, lhs, rhs, name=None):
""" Creates a "greater than or equal to" linear constraint.
Note:
This method returns a constraint object that is not added to the model.
To add it to the model, use the :func:`add_constraint` method.
Args:
lhs: An object that can be converted to a linear expression, typically a variable,
a member of an expression.
rhs: An object that can be converted to a linear expression, typically a variable,
a number of an expression.
name (string): An optional string to name the constraint.
:returns: An instance of :class:`docplex.mp.linear.LinearConstraint`.
"""
return self._lfactory.new_ge_constraint(lhs, rhs, name)
[docs] def eq_constraint(self, lhs, rhs, name=None):
""" Creates an equality constraint.
Note:
This method returns a constraint object that is not added to the model.
To add it to the model, use the :func:`add_constraint` method.
:param lhs: An object that can be converted to a linear expression, typically a variable,
a member of an expression.
:param rhs: An object that can be converted to a linear expression, typically a variable,
a member of an expression.
:param name: An optional string to name the constraint.
:returns: An instance of :class:`docplex.mp.linear.LinearConstraint`.
"""
return self._lfactory.new_eq_constraint(lhs, rhs, name)
[docs] def linear_constraint(self, lhs, rhs, ctsense, name=None):
""" Creates a linear constraint.
Note:
This method returns a constraint object that is not added to the model.
To add it to the model, use the :func:`add_constraint` method.
Args:
lhs: An object that can be converted to a linear expression, typically a variable,
a member of an expression.
rhs: An object that can be converted to a linear expression, typically a variable,
a number of an expression.
ctsense: A constraint sense; accepts either a
value of type `ComparisonType` or a string (e.g 'le', 'eq', 'ge').
name (string): An optional string to name the constraint.
:returns: An instance of :class:`docplex.mp.linear.LinearConstraint`.
"""
return self._lfactory.new_binary_constraint(lhs, ctsense, rhs, name)
def not_equal_constraint(self, lhs, rhs, name=None):
return self._lfactory.new_neq_constraint(lhs, rhs, name)
def _create_engine_constraint(self, ct):
# INTERNAL
eng = self.__engine
if isinstance(ct, LinearConstraint):
return eng.create_linear_constraint(ct)
elif isinstance(ct, RangeConstraint):
return eng.create_range_constraint(ct)
elif isinstance(ct, IndicatorConstraint):
# here check whether linear ct is trivial. if yes do not send to CPLEX
indicator = ct
linct = indicator.linear_constraint
if linct.is_trivial():
is_feasible = linct._is_trivially_feasible()
if is_feasible:
self.warning("Indicator constraint {0!s} has a trivially feasible constraint (no effect)",
indicator)
return -2
else:
self.warning(
"indicator constraint {0!s} has a trivially infeasible constraint; variable invalidated",
indicator)
indicator.invalidate()
return -4
return eng.create_logical_constraint(ct, is_equivalence=False)
elif isinstance(ct, EquivalenceConstraint):
return eng.create_logical_constraint(ct, is_equivalence=True)
elif isinstance(ct, QuadraticConstraint):
return eng.create_quadratic_constraint(ct)
elif isinstance(ct, PwlConstraint):
return eng.create_pwl_constraint(ct)
else:
self.fatal("Expecting binary constraint, indicator or range, got: {0!s}", ct) # pragma: no cover
def _notify_trivial_constraint(self, ct, ctname, is_feasible):
self_trivial_warn_level = self._checker.check_trivial_constraints()
if is_feasible and self_trivial_warn_level > warn_trivial_feasible:
return
elif self_trivial_warn_level > warn_trivial_infeasible:
return
# ---
# hereafter we are sure to warn
if ct is None:
arg = None
# elif ctname:
# arg = ctname
# elif ct.has_name():
# arg = ct.name
else:
arg = str_maxed(ct, maxlen=24)
# ---
ct_typename = ct.short_typename if ct is not None else "constraint"
ct_rank = self.number_of_constraints + 1
# BEWARE: do not use if arg here
# because if arg is a constraint, boolean conversion won't work.
trivial_msg = "Adding trivially {3}feasible {2}: '{0!s}', pos: {1}"
if arg is not None:
if is_feasible:
self.info(trivial_msg, arg, ct_rank, ct_typename, '')
else:
self.error(trivial_msg, arg, ct_rank, ct_typename, 'in')
docplex_add_trivial_infeasible_ct_here()
else:
if is_feasible:
self.info(trivial_msg, '', ct_rank, ct_typename, '')
else:
self.error(trivial_msg, '', ct_rank, ct_typename, 'in')
docplex_add_trivial_infeasible_ct_here()
def _register_ct_name(self, ct, ctname, arg_checker):
checker = arg_checker or self._checker
if ctname:
ct_name_map = self._cts_by_name
if ct_name_map is not None:
checker.check_duplicate_name(ctname, ct_name_map, "constraint")
ct_name_map[ctname] = ct
ct._set_name(ctname)
def _prepare_constraint(self, ct, ctname, check_for_trivial_ct, arg_checker=None):
# INTERNAL
checker = arg_checker or self._checker
if ct is True:
# sum([]) == 0
self._notify_trivial_constraint(ct=None, ctname=ctname, is_feasible=True)
return False
elif ct is False:
# happens with sum([]) and constant e.g. sum([]) == 2
self._notify_trivial_constraint(ct=None, ctname=ctname, is_feasible=False)
msg = "Adding a trivially infeasible constraint"
if ctname:
msg += ' with name: {0}'.format(ctname)
# analogous to 0 == 1, model is sure to fail
self.fatal(msg)
else:
checker.typecheck_ct_to_add(ct, self, 'add_constraint')
# -- watch for trivial cts e.g. linexpr(0) <= linexpr(1)
if check_for_trivial_ct and ct.is_trivial():
if ct._is_trivially_feasible():
self._notify_trivial_constraint(ct, ctname, is_feasible=True)
elif ct._is_trivially_infeasible():
self._notify_trivial_constraint(ct, ctname, is_feasible=False)
# check for already posted cts.
if not self._check_new_ct_index(ct):
return False
# --- name management ---
self._register_ct_name(ct, ctname, checker)
# ---
return True
def _check_new_ct_index(self, ct):
ok = True
if ct._index >= 0:
self.warning("constraint has already been posted: {0!s}, index is: {1}", ct, ct.index) # pragma: no cover
ok = False
return ok
def _check_trivial_constraints(self):
check_trivial = self._checker.check_trivial_constraints()
return check_trivial < warn_trivial_none
def _add_constraint_internal(self, ct, ctname=None):
used_ct_name = None if self._ignore_names else ctname
if not isinstance(ct, AbstractConstraint) and hasattr(ct, 'as_logical_operand'):
ct1 = self._lfactory.logical_expr_to_constraint(ct, ctname)
return self._post_constraint(ct1)
check_trivial = self._check_trivial_constraints()
if self._prepare_constraint(ct, used_ct_name, check_for_trivial_ct=check_trivial):
self._post_constraint(ct)
return ct
elif ct is True or ct is False:
return None
else:
return ct
def _post_constraint(self, ct):
ct_engine_index = self._create_engine_constraint(ct)
self._register_one_constraint(ct, ct_engine_index, is_ctname_safe=True)
return ct
def _remove_constraint_internal(self, ct):
self._remove_constraints_internal(cts_to_remove=(ct,))
def _resolve_ct(self, ct_arg, silent=False, check_index=True, caller=None):
verbose = not silent
if caller:
s_caller = caller + ": "
else:
s_caller = ""
ct = None
if isinstance(ct_arg, AbstractConstraint):
ct = ct_arg
elif is_string(ct_arg):
ct = self.get_constraint_by_name(ct_arg)
if ct is None and verbose:
self.error("{0}no constraint with name: \"{1}\" - ignored", s_caller, ct_arg)
elif is_int(ct_arg):
if ct_arg >= 0:
ct_index = ct_arg
ct = self.get_constraint_by_index(ct_index)
if ct is None and verbose:
self.error("{0}no constraint with index: \"{1}\" - ignored", s_caller, ct_arg)
else:
self.error("{0}not a valid index: \"{1}\" - ignored", s_caller, ct_arg)
else:
if verbose:
self.error("{0}unexpected argument {1!s}, expecting string or constraint", s_caller, ct_arg)
if ct is not None and ct.has_valid_index() or not check_index:
return ct
else:
return None
[docs] def remove_constraint(self, ct_arg):
""" Removes a constraint from the model.
Args:
ct_arg: The constraint to remove. Accepts either a constraint object or a string.
If passed a string, looks for a constraint with that name.
"""
ct = self._resolve_ct(ct_arg, silent=False, caller="remove_constraint")
if ct is not None:
self._checker.typecheck_in_model(self, ct, caller="constraint")
self._remove_constraints_internal(cts_to_remove=(ct,))
[docs] def clear_constraints(self):
"""
This method removes all constraints from the model.
"""
self.__engine.remove_constraints(cts=None) # special case to denote all
# clear containers
self._cts_by_name = None
# clear constraint index scopes.
for ctscope in self._iter_constraint_scopes():
ctscope.clear()
[docs] def remove_constraints(self, cts=None, error='warn'):
"""
This method removes a batch of constraints from the model.
:param cts: an iterable of constraints (linear, range, quadratic, indicators)
"""
if cts is not None:
# resolve constraints
if not is_iterable(cts):
self.fatal("Model.remove_constraints expects an iterable with constraints or strings")
lcts = []
for cta in cts:
ct = self._resolve_ct(cta, silent=True, caller='Model.remove_constraints')
if ct is not None:
lcts.append(ct)
elif error == 'raise':
self.fatal("Model.remove_constraints: cannot resolve this as a constraint: {0}", repr(cta))
elif error == 'warn':
self.warning("Model.remove_constraints: cannot resolve this as a constraint: {0}", repr(cta))
self._remove_constraints_internal(lcts)
def _remove_constraints_internal(self, cts_to_remove):
if cts_to_remove:
assert all(ct_.has_valid_index() for ct_ in cts_to_remove)
self.__engine.remove_constraints(cts_to_remove)
# INTERNAL
self_cts_by_name = self._cts_by_name
if self_cts_by_name:
for d in cts_to_remove:
dname = d.name
if dname:
try:
del self_cts_by_name[dname]
except KeyError:
pass
actual_touched_scopes = set()
#removed_ids = set()
from collections import defaultdict
idxs_by_scope = defaultdict(set)
for d in cts_to_remove:
#removed_ids.add(id(d))
idxs_by_scope[d.cplex_scope].add(d.index)
actual_touched_scopes.add(d._get_index_scope())
for sc, delset in idxs_by_scope.items():
scope = self._get_obj_scope(sc)
scope.notify_delete_set(delset)
for d in cts_to_remove:
d.notify_deleted()
if self.is_docplex_debug():
for sc in self._iter_constraint_scopes():
sc.check_indices()
[docs] def remove(self, removed):
"""
This method removes a constraint or a collection of constraints from the model.
:param removed: accapts either a constraint or an iterable on constraints (linear, range, quadratic, indicators)
"""
if is_iterable(removed):
self.remove_constraints(removed)
else:
self.remove_constraint(removed)
[docs] def add_range(self, lb, expr, ub, rng_name=None):
""" Adds a new range constraint to the model.
A range constraint states that a linear
expression has to stay within an interval `[lb..ub]`.
Both `lb` and `ub` have to be float numbers with `lb` smaller than `ub`.
The method creates a new range constraint and adds it to the model.
Args:
lb (float): A floating-point number.
expr: A linear expression, e.g. X+Y+Z.
ub (float): A floating-point number, which should be greater than `lb`.
rng_name (string): An optional name for the range constraint.
:returns: The newly created range constraint.
:rtype: :class:`docplex.mp.constr.RangeConstraint`
Raises:
An exception if `lb` is greater than `ub`.
"""
rng = self.range_constraint(lb, expr, ub)
ctname = None if self._ignore_names else rng_name
ct = self._add_constraint_internal(rng, ctname)
return ct
[docs] def indicator_constraint(self, binary_var, linear_ct, active_value=1, name=None):
""" Creates and returns a new indicator constraint.
The indicator constraint is not added to the model.
Args:
binary_var: The binary variable used to control the satisfaction of the linear constraint.
linear_ct: A linear constraint (EQ, LE, GE).
active_value: 0 or 1. The value used to trigger the satisfaction of the constraint. The default is 1.
name (string): An optional name for the indicator constraint.
:return:
The newly created indicator constraint.
"""
self._checker.typecheck_binary_var(binary_var)
self._checker.typecheck_linear_constraint(linear_ct)
self._checker.typecheck_zero_or_one(active_value)
self._checker.typecheck_in_model(self, binary_var, caller="binary variable")
self._checker.typecheck_in_model(self, linear_ct, caller="linear_constraint")
return self._lfactory.new_indicator_constraint(binary_var, linear_ct, active_value, name)
[docs] def add_indicator(self, binary_var, linear_ct, active_value=1, name=None):
""" Adds a new indicator constraint to the model.
An indicator constraint links (one-way) the value of a binary variable to
the satisfaction of a linear constraint.
If the binary variable equals the active value, then the constraint is satisfied, but
otherwise the constraint may or may not be satisfied.
Args:
binary_var: The binary variable used to control the satisfaction of the linear constraint.
linear_ct: A linear constraint (EQ, LE, GE).
active_value: 0 or 1. The value used to trigger the satisfaction of the constraint. The default is 1.
name (string): An optional name for the indicator constraint.
Returns:
The newly created indicator constraint.
"""
self._checker.typecheck_string(name, accept_none=True)
iname = None if self._ignore_names else name
indicator = self.indicator_constraint(binary_var, linear_ct, active_value)
return self._add_indicator(indicator, iname)
_indicator_trivial_feasible_idx = -2
_indicator_trivial_infeasible_idx = -4
def _add_indicator(self, indicator, ind_name, check_trivials=False):
# INTERNAL
linear_ct = indicator.linear_constraint
if check_trivials and self._checker.check_trivial_constraints() and linear_ct.is_trivial():
is_feasible = linear_ct._is_trivially_feasible()
if is_feasible:
self.warning("Indicator constraint {0!s} has a trivial feasible linear constraint (has no effect)",
indicator)
return self._indicator_trivial_feasible_idx
else:
self.warning("indicator constraint {0!s} has a trivial infeasible linear constraint - invalidated",
indicator)
indicator.invalidate()
return self._indicator_trivial_infeasible_idx
else:
return self._add_constraint_internal(indicator, ind_name)
[docs] def add_equivalence(self, binary_var, linear_ct, true_value=1, name=None):
""" Adds a new equivalence constraint to the model.
An equivalence constraints links two-way the value of a binary variable to
the satisfaction of a discrete linear constraint.
If the binary variable equals the true value, then the constraint is satisfied,
conversely if the constraint is satisfied, the binary variable is equal to the true value.
Args:
binary_var: The binary variable used to control the satisfaction of the linear constraint.
linear_ct: A linear constraint (EQ, LE, GE).
true_value: 0 or 1. The value used to trigger the satisfaction of the constraint. The default is 1.
name (string): An optional name for the equivalence constraint.
Returns:
The newly created equivalence constraint.
"""
equiv = self.equivalence_constraint(binary_var, linear_ct, true_value, name=None)
eq_name = None if self.ignore_names else name
eqct = self._add_constraint_internal(equiv, eq_name)
return eqct
[docs] def equivalence_constraint(self, binary_var, linear_ct, true_value=1, name=None):
""" Creates and returns a new equivalence constraint.
The newly created equivalence constraint is not added to the model.
Args:
binary_var: The binary variable used to control the satisfaction of the linear constraint.
linear_ct: A linear constraint (EQ, LE, GE).
true_value: 0 or 1. The value used to mark the satisfaction of the constraint. The default is 1.
name (string): An optional name for the equivalence constraint.
Returns:
The newly created equivalence constraint.
"""
checker = self._checker
checker.typecheck_binary_var(binary_var)
checker.typecheck_linear_constraint(linear_ct)
checker.typecheck_zero_or_one(true_value)
checker.typecheck_in_model(self, binary_var, caller="binary variable")
checker.typecheck_in_model(self, linear_ct, caller="linear_constraint")
checker.typecheck_string(name, accept_empty=True, accept_none=True)
StaticTypeChecker.typecheck_discrete_constraint(self, linear_ct,
msg='Model.add_equivalence() requires a discrete constraint')
used_name = None if self.ignore_names else name
equiv = self._lfactory.new_equivalence_constraint(binary_var, linear_ct, true_value, used_name)
return equiv
[docs] def add_equivalences(self, binary_vars, cts, true_values=1, names=None):
""" Adds a batch of equivalence constraints to the model.
This method adds a batch of equivalence constraints to the model.
:param binary_vars: a sequence of binary variables.
:param cts: a sequence of discrete linear constraints
:param true_values: the true values to use. Accepts either 1, 0 or a sequence of {0, 1} values.
:param names: an optional sequence of names
All sequences must have the same length.
:return: a list of equivalence constraints.
"""
return self._add_batch_logical_cts(binary_vars, cts, names, true_values, is_equivalence=True,
caller='Model.add_equivalences')
[docs] def add_indicators(self, binary_vars, cts, true_values=1, names=None):
""" Adds a batch of indicator constraints to the model.
This method adds a batch of indicator constraints to the model.
:param binary_vars: a sequence of binary variables.
:param cts: a sequence of discrete linear constraints
:param true_values: the true values to use. Accepts either 1, 0 or a sequence of {0, 1} values.
:param names: an optional sequence of names
All sequences must have the same length.
:return: a list of indicator constraints.
"""
return self._add_batch_logical_cts(binary_vars, cts, names, true_values, is_equivalence=False,
caller='Model.add_indicators')
def _add_batch_logical_cts(self, binary_vars, cts, names, true_values, is_equivalence, caller=''):
# internal
checker = self._checker
bvars = checker.typecheck_var_seq(binary_vars, vtype='B', caller=caller)
ctseq = checker.typecheck_constraint_seq(cts, check_linear=True)
try:
n_vars = len(bvars)
n_cts = len(cts)
except TypeError:
# if passed iterators, no len()
bvars = list(bvars)
ctseq = list(ctseq)
n_vars = len(bvars)
n_cts = len(ctseq)
if n_vars != n_cts:
self.fatal('Model.add_equivalences(): binary_vars and linear cts must have same size.')
if true_values == 0 or true_values == 1:
actual_true_values = generate_constant(true_values, n_vars)
elif is_iterable(true_values):
actual_true_values = list(true_values)
if len(actual_true_values) != n_vars:
self.fatal('Model.add_equivalences(): true_values has wrong size. expecting: {0}, got: {1}'
.format(n_vars, len(actual_true_values)))
for a in actual_true_values:
checker.typecheck_zero_or_one(a)
else:
self.fatal('Model.add_equivalence(): true_values expects 0|1 or sequence of {{0, 1}}, got: {0!r}'.format(true_values))
if names is not None and not self._ignore_names:
c_names = checker.typecheck_string_seq(names, accept_none=True, accept_empty=True, caller=caller)
used_names = [n or '' for n in c_names]
# checker.typecheck_string(n, accept_empty=False, accept_none=True)
# used_names.append(n or '')
else:
used_names = generate_constant(None, n_vars)
if is_equivalence:
# check discrete
lcts = list(ctseq)
caller = "Model.add_equivalences" if is_equivalence else "Model.add_indicators"
caller += " requires an iterable of discrete constraints"
for ct in lcts:
StaticTypeChecker.typecheck_discrete_constraint(self, ct, caller)
eqcts = self._lfactory.new_batch_equivalence_constraints(bvars, lcts, actual_true_values, used_names)
self.add_equivalence_constraints_(eqcts)
return eqcts
else:
indcts = self._lfactory.new_batch_indicator_constraints(bvars, ctseq, actual_true_values, used_names)
self.add_indicator_constraints_(indcts)
return indcts
[docs] def add_indicator_constraints(self, indcts):
""" Adds a batch of indicator constraints to the model
:param indcts: an iterable returning indicator constraints.
See Also:
:func:`indicator_constraint`
"""
ind_cts_list_ = list(indcts)
self._checker.typecheck_logical_constraint_seq(ind_cts_list_, true_if_equivalence=False)
ind_indices = self.__engine.create_batch_logical_constraints(ind_cts_list_, is_equivalence=False)
self._register_block_cts(self._logical_scope, ind_cts_list_, ind_indices)
return ind_cts_list_
add_indicator_constraints_ = add_indicator_constraints
[docs] def add_equivalence_constraints(self, eqcts):
""" Adds a batch of equivalence constraints to the model
:param eqcts: an iterable returning equivalence constraints.
See Also:
:func:`equivalence_constraint`
"""
eqcts_list_ = list(eqcts) # the list is traversed twice
self._checker.typecheck_logical_constraint_seq(eqcts_list_, true_if_equivalence=True)
eq_indices = self.__engine.create_batch_logical_constraints(eqcts_list_, is_equivalence=True)
self._register_block_cts(self._logical_scope, eqcts_list_, eq_indices)
return eqcts_list_
add_equivalence_constraints_ = add_equivalence_constraints
[docs] def if_then(self, if_ct, then_ct, negate=False):
""" Creates and returns an if-then constraint.
An if-then constraint links two constraints ct1, ct2 such that when ct1 is satisfied then ct2 is also satisfied.
:param if_ct: a linear constraint, the satisfaction of which governs the satisfaction of the `then_ct`
:param then_ct: a linear constraint, which becomes satisfied as soon as `if_ct` is satisfied
(or when it is not, depending on the `negate` flag).
:param negate: an optional boolean flag (default is False). If True, `then_ct` is satisfied when `if_ct` is *not* satisfied.
:return:
an instance of IfThenConstraint, that is not added to the model.
Use Model.add_constraint() or Model.add() to add it to the model.
Note:
This constraint relies on the status of the `if_ct` constraint, so this constraint must be discrete,
otherwise an exception will be raised.
"""
checker = self._checker
checker.typecheck_linear_constraint(if_ct)
checker.typecheck_linear_constraint(then_ct)
StaticTypeChecker.typecheck_discrete_constraint(logger=self, ct=if_ct, msg='Model.if_then()')
return self._lfactory.new_if_then_constraint(if_ct, then_ct, bool(negate))
[docs] def add_if_then(self, if_ct, then_ct, negate=False):
""" Creates a new if-then constraint and adds it to the model
:param if_ct: a linear constraint, the satisfaction of which governs the satisfaction of the `then_ct`
:param then_ct: a linear constraint, which becomes satisfied as soon as `if_ct` is satisfied
(or when it is not, depending on the `negate` flag).
:param negate: an optional boolean flag (default is False). If True, `then_ct` is satisfied when `if_ct` is *not* satisfied.
:return:
an instance of IfThenConstraint.
Note:
This constraint relies on the status of the `if_ct` constraint, so this constraint must be discrete,
otherwise an exception will be raised. On the opposite, the `then_ct` constraint may be non-discrete.
Also note that this construct relies on the status variable of the `if_ct`, so one extra binary variable is generated.
*New in 2.16*: when `if_ct` is of the form `bvar == 1` or `bvar ==0`, where `bvar` is a binary variable,
, no extra variable is generated, and a plain indicator constraint is generated.
An alternative syntax is to use the `>>` operator on linear constraints:
>>> m.add(c1 >> c2)
is exactly equivalent to:
>>> m.add_if_then(c1, c2)
"""
ifthen_ct = self.if_then(if_ct, then_ct, negate=negate)
return self._post_constraint(ifthen_ct)
[docs] def range_constraint(self, lb, expr, ub, rng_name=None):
""" Creates a new range constraint but does not add it to the model.
A range constraint states that a linear
expression has to stay within an interval `[lb..ub]`.
Both `lb` and `ub` have to be floating-point numbers with `lb` smaller than `ub`.
The method creates a new range constraint but does not add it to the model.
Args:
lb: A floating-point number.
expr: A linear expression, e.g. X+Y+Z.
ub: A floating-point number, which should be greater than `lb`.
rng_name: An optional name for the range constraint.
Returns:
The newly created range constraint.
Raises:
An exception if `lb` is greater than `ub`.
"""
self._checker.typecheck_num(lb, 'Model.range_constraint')
self._checker.typecheck_num(ub, 'Model.range_constraint')
self._checker.typecheck_string(rng_name, accept_empty=False, accept_none=True)
rng = self._lfactory.new_range_constraint(lb, expr, ub, rng_name)
return rng
[docs] def add_constraint(self, ct, ctname=None):
""" Adds a new linear constraint to the model.
Args:
ct: A linear constraint of the form <expr1> <op> <expr2>, where both expr1 and expr2 are
linear expressions built from variables in the model, and <op> is a relational operator
among <= (less than or equal), == (equal), and >= (greater than or equal).
ctname (string): An optional string used to name the constraint.
Returns:
The newly added constraint.
See Also:
:func:`add_constraint_`
"""
ct = self._add_constraint_internal(ct, ctname)
return ct
[docs] def add_constraint_(self, ct, ctname=None):
""" Adds a new linear constraint to the model.
Args:
ct: A linear constraint of the form <expr1> <op> <expr2>, where both expr1 and expr2 are
linear expressions built from variables in the model, and <op> is a relational operator
among <= (less than or equal), == (equal), and >= (greater than or equal).
ctname (string): An optional string used to name the constraint.
Note:
This method does the same as `docplex.mp.model.Model.add_constraint()` except that it has no return value.
See Also:
:func:`add_constraint`
"""
self._add_constraint_internal(ct, ctname)
def add(self, ct, name=None):
if is_iterable(ct):
return self.add_constraints(ct, name)
else:
return self.add_constraint(ct, name)
def add_(self, ct, name=None):
if is_iterable(ct):
self.add_constraints_(ct, name)
else:
self.add_constraint_(ct, name)
[docs] def add_constraints(self, cts, names=None):
""" Adds a batch of linear constraints to the model in one operation.
Each constraint from the `cts` iterable is added to the model.
If present, the `names` iterable is used to set names to the constraints.
Example:
# list
>>> m.add_constraints([x >= 1, y<= 3], ["c1", "c2"])
# comprehension
>>> m.add_constraints((xs[i] >= i for i in range(N)))
Args:
cts: An iterable of linear constraints; can be a list, a set or a comprehensions.
Any Python object, which can be iterated on and yield consttraint objects.
names: An optional iterable on strings. ANy Python object which can be iterated on
and yield strings. The default value is None, meaning no names are set.
Returns:
A list of the newly added constraints.
Note:
This method handles only linear constraints (including range constraints). To add
multiple quadratic constraints, see :func:`add_quadratic_constraints`
See Also:
:func:`add_constraints_`
"""
self._checker.typecheck_iterable(cts)
if names is not None and not self.ignore_names:
if not is_iterable(names) or (is_string(names) and not names):
self.fatal("Model.add_constraints() expects a sequence of strings or a non-empty string")
return self._lfactory._new_constraint_block2(cts, names)
else:
return self._lfactory._new_constraint_block1(cts)
def vector_compare(self, lhss, rhss, sense):
l_lhs = ordered_sequence_to_list(lhss, caller='Model.vector.compare')
l_rhs = ordered_sequence_to_list(rhss, caller='Model.vector.compare')
if len(l_lhs) != len(l_rhs):
self.fatal('Model.vector_compare() requires two lists with same length, left size: {0}, right size: {1}'.
format(len(l_lhs), len(l_rhs)))
ctsense = ComparisonType.parse(sense)
return self._aggregator._vector_compare(l_lhs, l_rhs, ctsense)
def vector_compare_le(self, lhss, rhss):
return self.vector_compare(lhss, rhss, 'le')
def vector_compare_ge(self, lhss, rhss):
return self.vector_compare(lhss, rhss, 'ge')
def vector_compare_eq(self, lhss, rhss):
return self.vector_compare(lhss, rhss, 'eq')
def _new_xconstraint(self, lhs, rhs, comparaison_type):
isquad = False
if self._quad_count:
try:
isquad = rhs.is_quad_expr()
except AttributeError:
pass
if isquad:
return self._qfactory._new_qconstraint(lhs, comparaison_type, rhs)
else:
return self._lfactory._new_binary_constraint(lhs, comparaison_type, rhs)
[docs] def add_constraints_(self, cts, names=None):
""" Adds a batch of linear constraints to the model in one operation.
Same as `docplex.model.Model.add_constraints()` except that is does not return anything.
"""
self._checker.typecheck_iterable(cts)
if names is not None and not self.ignore_names:
self._lfactory._new_constraint_block2(cts, names)
else:
self._lfactory._new_constraint_block1(cts)
def add_ranges(self, lbs, exprs, ubs, names=None):
checker = self._checker
lbsl = checker.typecheck_num_seq(lbs, caller="Model.add_ranges.lbs")
ubsl = checker.typecheck_num_seq(ubs, caller="Model.add_ranges.ubs")
checker.typecheck_iterable(exprs)
range_names = names if names and not self.ignore_names else None
return self._lfactory.new_range_block(lbsl, exprs, ubsl, range_names)
def _post_quadratic_constraint(self, qct):
qcx = self.__engine.create_quadratic_constraint(qct)
self._register_one_constraint(qct, qcx, is_ctname_safe=True)
return qct
[docs] def add_quadratic_constraints(self, qcs):
""" Adds a batch of quadratic contraints in one call.
:param qcs: an iterable on a quadratic constraints.
Note:
The `Model.add_constraints` method handles only linear constraints.
New in version 2.16*
"""
lqcs = self._checker.typecheck_quadratic_constraint_seq(qcs)
for qc in lqcs:
self._post_quadratic_constraint(qc)
# ----------------------------------------------------
# objective
# ----------------------------------------------------
[docs] def minimize(self, expr):
""" Sets an expression as the expression to be minimized.
The argument is converted to a linear expression. Accepted types are variables (instances of
:class:`docplex.mp.dvar.Var` class), linear expressions (instances of
:class:`docplex.mp.linear.LinearExpr`), or numbers.
:param expr: A linear expression or a variable.
"""
self.set_objective(ObjectiveSense.Minimize, expr)
[docs] def maximize(self, expr):
"""
Sets an expression as the expression to be maximized.
The argument is converted to a linear expression. Accepted types are variables (instances of
:class:`docplex.mp.dvar.Var` class), linear expressions (instances of
:class:`docplex.mp.linear.LinearExpr`), or numbers.
:param expr: A linear expression or a variable.
"""
self.set_objective(ObjectiveSense.Maximize, expr)
def _make_lex_priorities(self, nb_objectives):
# INTERNAL
return list(range(nb_objectives-1, -1, -1))
def _compile_multiobj_expr_list(self, exprs, caller, accept_empty=False):
# INTERNAL
# converts an exprs argument to a (possibly empty) list of expressions.
# no side-effect is performed here, the caller has to take action.
caller_string = "{0} ".format(caller) if caller is not None else ""
if not exprs:
if accept_empty:
self.warning("{0}requires a non-empty list of linear expressions, got: {1!r}",
caller_string, exprs)
return []
else:
self.fatal("{0}requires a non-empty list of linear expressions, got: {1!r}",
caller_string, exprs)
if is_indexable(exprs):
try:
exprs = [self._lfactory._to_linear_operand(x) for x in exprs]
return exprs
except (TypeError, DOcplexException):
pass
self.fatal(
"{0}requires an indexable sequence of linear expressions, {1!r} was passed",
caller_string, exprs)
@classmethod
def supports_multi_objective(cls):
return cls()._supports_multi_objective()
def _supports_multi_objective(self):
# INTERNAL
ok, _ = self.__engine.supports_multi_objective()
return ok
def _check_multi_objective_support(self):
# INTERNAL
ok, why = self.__engine.supports_multi_objective()
if not ok:
assert why
self.fatal(msg=why)
[docs] def minimize_static_lex(self, exprs, abstols=None, reltols=None, objnames=None):
""" Sets a list of expressions to be minimized in a lexicographic solve.
exprs must be an ordered sequence of objective functions, that are minimized.
The argument is converted to a list of linear expressions. Accepted types for the list elements are variables
(instances of :class:`docplex.mp.dvar.Var` class), linear expressions (instances of
:class:`docplex.mp.linear.LinearExpr`), or numbers.
Warning:
This method requires CPLEX 12.9 or higher
Args:
exprs: a list of linear expressions or variables
abstols: if defined, a list of absolute tolerances having the same size as the `exprs` argument.
reltols: if defined, a list of relative tolerances having the same size as the `exprs` argument.
objnames: if defined, a list of names for objectives having the same size as the `exprs` argument.
*New in version 2.9*
"""
self._set_lex_multi_objective(ObjectiveSense.Minimize, exprs, abstols=abstols, reltols=reltols, names=objnames,
caller='Model.minimize_static_lex()')
[docs] def maximize_static_lex(self, exprs, abstols=None, reltols=None, objnames=None):
""" Sets a list of expressions to be maximized in a lexicographic solve.
exprs defines an ordered sequence of objective functions that are maximized.
The argument is converted to a list of linear expressions. Accepted types for the list elements are variables
(instances of :class:`docplex.mp.dvar.Var` class), linear expressions (instances of
:class:`docplex.mp.linear.LinearExpr`), or numbers.
Warning:
This method requires CPLEX 12.9 or higher
Args:
exprs: a list of linear expressions or variables
abstols: if defined, a list of absolute tolerances having the same size as the `exprs` argument.
reltols: if defined, a list of relative tolerances having the same size as the `exprs` argument.
objnames: if defined, a list of names for objectives having the same size as the `exprs` argument.
*New in version 2.9*
"""
self._set_lex_multi_objective(ObjectiveSense.Maximize, exprs, abstols=abstols, reltols=reltols, names=objnames,
caller='Model.maximize_static_lex()')
[docs] def is_minimized(self):
""" Checks whether the model is a minimization model.
Note:
This returns True even if the expression to minimize is a constant.
To check whether the model has a non-constant objective, use :func:`is_optimized`.
Returns:
Boolean: True if the model is a minimization model.
"""
return self._objective_sense is ObjectiveSense.Minimize
[docs] def is_maximized(self):
""" Checks whether the model is a maximization model.
Note:
This returns True even if the expression to maximize is a constant.
To check whether the model has a non-constant objective, use :func:`is_optimized`.
Returns:
Boolean: True if the model is a maximization model.
"""
return self._objective_sense is ObjectiveSense.Maximize
[docs] def objective_coef(self, dvar):
""" Returns the objective coefficient of a variable.
The objective coefficient is the coefficient of the given variable in
the model's objective expression. If the variable is not explicitly
mentioned in the objective, it returns 0.
:param dvar: The decision variable for which to compute the objective coefficient.
Returns:
float: The objective coefficient of the variable.
"""
self._checker.typecheck_var(dvar)
return self._objective_coef(dvar)
def _objective_coef(self, dvar):
return self._objective_expr.unchecked_get_coef(dvar)
[docs] def remove_objective(self):
""" Clears the current objective.
This is equivalent to setting "minimize 0".
Any subsequent solve will look only for a feasible solution.
You can detect this state by calling :func:`has_objective` on the model.
"""
self.set_objective(self.default_objective_sense, self._new_default_objective_expr())
[docs] def is_optimized(self):
""" Checks whether the model has a non-constant objective expression.
A model with a constant objective will only search for a feasible solution when solved.
This happens either if no objective has been assigned to the model,
or if the objective has been removed with :func:`remove_objective`.
Returns:
Boolean: True, if the model has a non-constant objective expression.
"""
return self.has_multi_objective() or not self._objective_expr.is_constant()
[docs] def set_multi_objective(self, sense, exprs, priorities=None, weights=None,
abstols=None, reltols=None,
names=None):
""" Sets a list of objectives.
Warning:
This method requires CPLEX 12.9 or higher
Args:
sense: Either an instance of :class:`docplex.mp.basic.ObjectiveSense` (Minimize or Maximize),
or a string: "min" or "max".
exprs: Is converted to a list of expressions. Accepted types for this list items are variables,
linear expressions or numbers.
priorities: a list of priorities having the same size as the `exprs` argument. Priorities
define how objectives are grouped together into sub-problems, and in which order these sub-problems
are solved (in decreasing order of priorities). If not defined, allexpressions are assumed to share the same priority,
and are combined with `weights`.
weights: if defined, a list of weights having the same size as the `exprs` argument. Weights define
how objectives with same priority are blended together to define the associated sub-problem's
objective that is optimized. If not defined, weights are assumed to be all equal to 1.
abstols: if defined, a list of absolute tolerances having the same size as the `exprs` argument.
reltols: if defined, a list of relative tolerances having the same size as the `exprs` argument.
names: if defined, a list of names for objectives having the same size as the `exprs` argument.
Note:
When using a number for an objective, the search will not optimize but only look for a feasible solution.
*New in version 2.9.*
"""
self.set_objective_sense(sense)
self._set_multi_objective_exprs(exprs, priorities=priorities, weights=weights,
abstols=abstols, reltols=reltols, names=names,
caller='Model.set_multi_objective()')
[docs] def set_lex_multi_objective(self, sense, exprs, abstols=None, reltols=None, names=None):
""" Sets a list of objectives to be solved in a lexicographic fashion.
Objective expressions are listed in decreasing priority.
Warning:
This method requires CPLEX 12.9 or higher
Args:
sense: Either an instance of :class:`docplex.mp.basic.ObjectiveSense` (Minimize or Maximize),
or a string: "min" or "max".
exprs: Is converted to a list of expressions. Accepted types for this list items are variables,
linear expressions or numbers.
abstols: if defined, a list of absolute tolerances having the same size as the `exprs` argument.
reltols: if defined, a list of relative tolerances having the same size as the `exprs` argument.
names: if defined, a list of names for objectives having the same size as the `exprs` argument.
Note:
When using a number for an objective, the search will not optimize but only look for a feasible solution.
*New in version 2.9.*
"""
self._set_lex_multi_objective(sense, exprs, abstols=abstols, reltols=reltols, names=names, caller='Model.set_lex_multi_objective')
def _set_lex_multi_objective(self, sense, exprs, abstols, reltols, names, caller):
# INTERNAL
self.set_objective_sense(sense)
lex_exprs = self._compile_multiobj_expr_list(exprs, caller=caller, accept_empty=False)
lex_priorities = self._make_lex_priorities(len(exprs))
self._set_multi_objective_internal(lex_exprs, priorities=lex_priorities,
weights=None,
abstols=abstols, reltols=reltols, names=names,
caller=caller)
[docs] def set_multi_objective_exprs(self, exprs, priorities=None, weights=None,
abstols=None, reltols=None, names=None):
""" Defines a list of blended objectives.
Objectives with the same priority are combined using weights. Then, objectives are optimized in a
lexicographic fashion by decreasing priority.
Args:
exprs: Is converted to a list of linear expressions. Accepted types for this list items are variables,
linear expressions or numbers.
priorities: if defined, a list of priorities having the same size as the `exprs` argument. Priorities
define how objectives are grouped together into sub-problems, and in which order these sub-problems
are solved (in decreasing order of priorities).
weights: if defined, a list of weights having the same size as the `exprs` argument. Weights define
how objectives with same priority are blended together to define the associated sub-problem
objective that is optimized.
abstols: if defined, a list of absolute tolerances having the same size as the `exprs` argument.
reltols:if defined, a list of relative tolerances having the same size as the `exprs` argument.
names: if defined, a list of names for objectives having the same size as the `exprs` argument.
Note:
When using a number for an objective, the search will not optimize but only look for a feasible solution.
*New in version 2.9.*
"""
self._set_multi_objective_exprs(exprs, priorities, weights, abstols, reltols, names, caller='Model.set_multi_objective()')
def _set_multi_objective_exprs(self, exprs, priorities=None, weights=None,
abstols=None, reltols=None, names=None, caller=None):
exprs_ = self._compile_multiobj_expr_list(exprs, accept_empty=False, caller=caller)
if priorities:
# check an array of len(exprs)
priorities_ = priorities
else:
priorities_ = [1] * len(exprs_)
self._set_multi_objective_internal(exprs_, priorities=priorities_, weights=weights,
abstols=abstols, reltols=reltols, names=names, clear_objective=True,
caller=caller)
def _set_multi_objective_internal(self, exprs, priorities, weights,
abstols, reltols,
names, clear_objective=True, caller=None):
# INTERNAL: assumes exprs is a valid list of linear expressions
if 1 == len(exprs):
expr0 = exprs[0]
self.warning('Multi-objective has been converted to single objective: {0}', str_maxed(expr0, maxlen=16))
self.set_objective_expr(expr0)
else:
for x in exprs:
x.notify_used(self)
if self.has_objective() and clear_objective:
self._clear_objective_expr()
def refine_caller(caller_, qualifier):
if not caller_:
return caller_
elif caller_[-1] == ')':
return '%s.%s' % (caller_[:-2], qualifier)
else:
return '%s.%s' % (caller_, qualifier)
abstols_ = self._typecheck_optional_num_seq(abstols, accept_none=True, caller=refine_caller(caller, 'abstols'))
reltols_ = self._typecheck_optional_num_seq(reltols, accept_none=True, caller=refine_caller(caller, 'reltols'))
self._set_engine_multi_objective_exprs(exprs, priorities, weights, abstols_, reltols_, names)
self._multi_objective.update(exprs, priorities, weights, abstols, reltols, names)
def _clear_multi_objective(self):
# INTERNAL
zero_exprs = [self._new_default_objective_expr()]
self._set_engine_multi_objective_exprs(zero_exprs, priorities=[0], weights=[1],
abstols=[0], reltols=[0], names=[None])
self._multi_objective.clear()
def _nth_multi_objective(self, multiobj_index):
return self._multi_objective[multiobj_index]
def _check_has_multi_objective(self, caller):
if not self.has_multi_objective():
self.fatal("{0} requires model with multi-objective models", caller)
[docs] def set_multi_objective_abstols(self, abstols):
""" Changes absolute tolerances for multiple objectives.
Args:
abstols: new absolute tolerances. Can be either a number (applies to all objectives),
or a sequence of numbers.
A sequence must have the same length as the number of objectives.
*New in version 2.16*
"""
self._check_has_multi_objective(caller='Model.set_multi_objective_abstols')
nb_objectives = self.number_of_multi_objective_exprs
abstols_ = self._typecheck_optional_num_seq(abstols, accept_none=False, expected_size=nb_objectives)
self.get_engine().set_multi_objective_tolerances(abstols_, reltols=None)
# update tolerances in multi obj
self._multi_objective.update(new_abstols=abstols_, new_reltols=None)
[docs] def set_multi_objective_reltols(self, reltols):
""" Changes relative tolerances for multiple objectives.
Args:
reltols: new relative tolerances. Can be either a number (applies to all objectives),
or a sequence of numbers.
A sequence must have the same length as the number of objectives.
*New in version 2.16*
"""
self._check_has_multi_objective(caller='Model.set_multi_objective_reltols')
nb_objectives = self.number_of_multi_objective_exprs
reltols_ = self._typecheck_optional_num_seq(reltols, accept_none=False, expected_size=nb_objectives)
self.get_engine().set_multi_objective_tolerances(abstols=None, reltols=reltols_)
# update tolerances in multi obj
self._multi_objective.update(new_abstols=None, new_reltols=reltols_)
def _set_engine_multi_objective_exprs(self, exprs, priorities, weights, abstols, reltols, names):
# INTERNAL
old_multi_objective_exprs = self._multi_objective.exprs
eng = self.__engine
if eng:
nb_exprs = len(exprs)
eng.set_multi_objective_exprs(new_multiobjexprs=exprs,
old_multiobjexprs=old_multi_objective_exprs,
priorities=priorities, weights=weights,
abstols=MultiObjective.as_optional_sequence(abstols, nb_exprs),
reltols=MultiObjective.as_optional_sequence(reltols, nb_exprs),
objnames=names)
if old_multi_objective_exprs is not None:
for expr in old_multi_objective_exprs:
expr.notify_unsubscribed(subscriber=self)
[docs] def clear_multi_objective(self):
""" Clears everything related to multi-objective, if any.
If the model had previously defined
multi-objectives, resets the model with an objective of zero.
If the model had not defined multi-objectives, this method does nothing.
*New in version 2.10*
"""
self._clear_multi_objective()
def has_objective(self):
# INTERNAL
return not self._objective_expr.is_zero()
[docs] def has_multi_objective(self):
""" Returns True if the model has multi objectives defined
*New in version 2.10*
"""
return not self._multi_objective.empty()
@property
def number_of_multi_objective_exprs(self):
return self._multi_objective.number_of_objectives
def iter_multi_objective_tuples(self):
return self._multi_objective.itertuples()
def iter_multi_objective_exprs(self):
return self._multi_objective.iter_exprs()
[docs] def set_objective(self, sense, expr):
""" Sets a new objective.
Args:
sense: Either an instance of :class:`docplex.mp.basic.ObjectiveSense` (Minimize or Maximize),
or a string: "min" or "max".
expr: Is converted to an expression. Accepted types are variables,
linear expressions, quadratic expressions or numbers.
Note:
When using a number, the search will not optimize but only look for a feasible solution.
"""
self.set_objective_sense(sense)
self.set_objective_expr(expr)
def set_objective_sense(self, sense):
actual_sense = self._resolve_sense(sense)
self._objective_sense = actual_sense
eng = self.__engine
if eng:
# when ending the model, the engine is None here
eng.set_objective_sense(actual_sense)
@property
def objective_sense(self):
""" This property is used to get or set the direction of the optimization as an instance of
:class:`docplex.mp.basic.ObjectiveSense`, either Minimize or Maximize.
This property also accepts strings as arguments: 'min' for minimize and 'max' for maximize.
"""
return self._objective_sense
@objective_sense.setter
def objective_sense(self, new_sense):
self.set_objective_sense(new_sense)
def set_objective_expr(self, new_objexpr, clear_multiobj=True):
# INTERNAL
if self.has_multi_objective() and clear_multiobj:
# Need also to set all attributes to default values so that the model won't be treated as multi-objective
self._clear_multi_objective()
if new_objexpr is None:
expr = self._new_default_objective_expr()
else:
expr = self._lfactory._to_expr(new_objexpr)
#expr.keep()
expr.notify_used(self)
eng = self.__engine
current_objective_expr = self._objective_expr
if eng:
# when ending the model, the engine is None here
eng.set_objective_expr(expr, current_objective_expr)
if current_objective_expr is not None:
current_objective_expr.notify_unsubscribed(subscriber=self)
self._objective_expr = expr
def _clear_objective_expr(self):
# INTERNAL
current_objective_expr = self._objective_expr
if not current_objective_expr.is_zero():
eng = self.__engine
current_objective_expr = self._objective_expr
zero_expr = self._new_default_objective_expr()
if eng:
# when ending the model, the engine is None here
eng.set_objective_expr(new_objexpr=zero_expr, old_objexpr=current_objective_expr)
if current_objective_expr is not None:
current_objective_expr.notify_unsubscribed(subscriber=self)
self._objective_expr = zero_expr
[docs] def get_objective_expr(self):
""" This method returns the expression used as the model objective.
Note:
The default objective is a constant zero expression.
Returns:
an expression.
"""
return self._objective_expr
@property
def objective_expr(self):
""" This property is used to get or set the expression used as the model objective.
"""
return self._objective_expr
@objective_expr.setter
def objective_expr(self, new_expr):
self.set_objective_expr(new_expr)
def notify_expr_modified(self, expr, event):
# INTERNAL
objexpr = self._objective_expr
if event and expr is objexpr or expr is objexpr.linear_part:
# old and new are the same
self.__engine.update_objective(expr=expr, event=event)
def notify_expr_replaced(self, old_expr, new_expr):
if old_expr is self._objective_expr:
self.__engine.set_objective_expr(new_objexpr=new_expr, old_objexpr=old_expr)
if not is_var(new_expr):
new_expr.grab_subscribers(old_expr)
def _new_default_objective_expr(self):
# INTERNAL
return self._lfactory.linear_expr(arg=None, constant=0, safe=True)
@property
def default_objective_sense(self):
return ObjectiveSense.Minimize
def _can_solve(self):
return self.has_cplex()
def _make_end_infodict(self): # pragma: no cover
return self.solution.as_name_dict() if self.solution is not None else dict()
def prepare_actual_context(self, **kwargs):
# prepares the actual context that will be used for a solve
# use the provided context if any, or the self.context otherwise
if not kwargs:
return self.context
arg_context = kwargs.get('context') or self.context
if not isinstance(arg_context, Context):
self.fatal('Expecting instance of docplex.mp.Context, {0!r} was passed', arg_context)
cloned = False
context = arg_context
# update the context with provided kwargs
for argname, argval in kwargs.items():
if argname == 'clean_before_solve':
pass
elif argname != "context" and argval is not None:
if not cloned:
context = context.override()
cloned = True
context.update_key_value(argname, argval)
return context
[docs] def build_multiobj_paramsets(self, timelimits = None, mipgaps = None):
""" Creates a sequence containing pre-filled `ParameterSet` objects to be used with multi objective optimization
only.
Args:
lex_timelimits (optional): a sequence of time limits
lex_mipgaps (optional): a sequence of mip gaps
"""
return self.__engine._build_multiobj_paramsets(self, timelimits, mipgaps)
[docs] def create_parameter_sets(self):
""" Creates a sequence containing empty `ParameterSet` objects to be used with multi objective optimization
only.
"""
return self.__engine._create_parameter_sets(self)
[docs] def solve(self, **kwargs):
""" Starts a solve operation on the model.
Args:
context (optional): An instance of context to be used in instead of
the context this model was built with.
cplex_parameters (optional): A set of CPLEX parameters to use
instead of the parameters defined as
``context.cplex_parameters``.
Accepts either a RootParameterGroup object (obtained by cloning the model's
parameters), or a dict of path-like names and values.
checker (optional): a string which controls which type of checking is performed.
Possible values are:
- 'std' (the default) performs type checks on arguments to methods; checks that numerical
arguments are numbers, but will not check for NaN or infinity.
- 'numeric' checks that numerical arguments are valid numbers, neither NaN nor
math.infinity
- 'full' performs all possible checks, the union of 'std' and 'numeric' checks.
- 'off' performs no checking at all. Disabling all checks might improve performance, but only when
it is safe to do so.
log_output (optional): if ``True``, solver logs are output to
stdout. If this is a stream, solver logs are output to that
stream object. Overwrites the ``context.solver.log_output``
parameter.
clean_before_solve (optional): a boolean (default is False).
Solve normally picks up where the previous solve left, but if this flag is set to ``True``,
a fresh solve is started, forgetting all about previous solves..
parameter_sets (optional) an iterable of parameterset to be used with multi objective optimization.
See :func:`create_parameter_sets`
Returns:
A :class:`docplex.mp.solution.SolveSolution` object if the solve operation managed to create
a feasible solution, else None.
The reason why solve returned None includes not only errors, but also proper cases of infeasibilties
or unboundedness. When solve returns None, use Model.solve_details to check the status
of the latest solve operation: Model.solve_details always returns
a :class:`docplex.mp.sdetails.SolveDetails` object, whether or not
a solution has been found.
This object contains detailed information about the latest solve operation, such as status, elapsed time,
and for MILP problems, number of nodes processed and final gap.
See Also:
:func:`solve_details`
:class:`docplex.mp.sdetails.SolveDetails`
"""
if not self.is_optimized():
self.info("No objective to optimize - searching for a feasible solution")
parameter_sets = kwargs.pop('parameter_sets', None)
context = self.prepare_actual_context(**kwargs)
# log stuff
a_stream = context.solver.log_output_as_stream
with OverridenOutputContext(self, a_stream):
if self.environment.has_cplex:
# take arg clean flag or this model's
used_clean_before_solve = kwargs.get('clean_before_solve', self.clean_before_solve)
return self._solve_local(context, used_clean_before_solve, parameter_sets)# lex_timelimits, lex_mipgaps)
else:
return self.fatal("Cannot solve model: no CPLEX runtime found.")
def _connect_progress_listeners(self):
self.__engine.connect_progress_listeners(self, self._progress_listeners, self._qprogress_listeners)
def _disconnect_progress_listeners(self):
map (lambda xl: xl._disconnect(), chain(self._progress_listeners, self._qprogress_listeners))
def _notify_solve_hit_limit(self, solve_details):
# INTERNAL
if solve_details and solve_details.has_hit_limit():
self.info("solve: {0}".format(solve_details.status))
def _solve_local(self, context, clean_before_solve=None, parameter_sets = None):# lex_timelimits=None, lex_mipgaps=None):
""" Starts a solve operation on the local machine.
Note: If CPLEX is not available, an error is raised.
Args:
context: a (possibly new) context whose parameters override those of the modle
during this solve.
Returns:
A Solution object if the solve operation succeeded, None otherwise.
"""
local_solve_env = CplexLocalSolveEnv(self)
params_to_use = local_solve_env.before_solve(context)
self_engine = self.__engine
new_solution = None
try:
used_parameters = params_to_use or self.context._get_raw_cplex_parameters()
# assert used_parameters is not None
self._apply_parameters_to_engine(used_parameters)
new_solution = self_engine.solve(self,
parameters=used_parameters,
clean_before_solve=clean_before_solve,
parameter_sets = parameter_sets)
# store solve status as returned by the engine.
engine_status = self_engine.get_solve_status()
self._last_solve_status = engine_status
except DOcplexException as docpx_e: # pragma: no cover
new_solution = None
raise docpx_e
except Exception as e:
new_solution = None
print("----------------- Python exception: {}".format(str(e)))
raise e
finally:
self._set_solution(new_solution)
local_solve_env.after_solve(context, new_solution, self_engine)
return new_solution
[docs] def get_solve_status(self):
""" Returns the solve status of the last successful solve.
If the model has been solved successfully, returns the status stored in the
model solution. Otherwise returns None.
:returns: The solve status of the last successful solve, a enumerated value of type
`docplex.utils.JobSolveStatus`
Note: The status returned by Cplex is stored as `status` in the solve_details of the model.
>>> m.solve_details.status
See Also:
:func:`docplex.mp.SolveDetails.status` to get the Cplex status as a string (eg. "optimal")
:func:`docplex.mp.SolveDetails.status_code` to get the Cplex status as an integer code..
"""
warnings.warn("Model.get_solve_status() is deprecated with cloud solve, use Model.solve_details instead", DeprecationWarning)
return self._last_solve_status
@property
def solve_status(self):
""" Returns the solve status of the last successful solve.
If the model has been solved successfully, returns the status stored in the
model solution. Otherwise returns None`.
:returns: The solve status of the last successful solve, a string, or None.
"""
warnings.warn("Model.solve_status is deprecated with cloud solve, use Model.solve_details instead", DeprecationWarning)
return self._last_solve_status
@property
def job_solve_status(self):
# INTERNAL WML
return self._last_solve_status
def notify_start_solve(self):
# INTERNAL
pass
def notify_solve_failed(self):
pass
@property
def solve_details(self):
"""
This property returns detailed information about the latest solve,
an instance of :class:`docplex.mp.solution.SolveDetails`.
When the latest solve did return a Solution instance, this property
returns the solve details corresponding to the solution; when no
solution has been found (in other terms, the latest solve operation
returned None), it still returns a SolveDetails object, containing a
CPLEX code identifying the reason why no solution could be found
(for example, infeasibility or unboundedness).
See Also:
:class:`docplex.mp.sdetails.SolveDetails`
"""
from copy import copy as shallow_copy
return shallow_copy(self._solve_details)
def get_solve_details(self):
return self.solve_details
def notify_solve_relaxed(self, relaxed_solution, solve_details):
# INTERNAL: used by relaxer
self._solve_details = solve_details
self._set_solution(relaxed_solution)
if relaxed_solution is not None:
self.notify_start_solve()
else:
self.notify_solve_failed()
def _resolve_sense(self, sense_arg):
return ObjectiveSense.parse(sense_arg, self.logger) # raise if invalid
[docs] def solve_with_goals(self, goals,
senses='min',
abstols=None,
reltols=None,
goal_names=None,
write_pass_files=False,
solution_callbackfn=None,
**kwargs):
""" Performs a solve from an ordered collection of goals.
:param goals: An ordered collection of linear expressions.
:param senses: Accepts ither an ordered sequence of senses, one sense, or
None. The default is None, in which case the solve uses a Minimize
sense. Each sense can be either a sense object, that is either
`ObjectiveSense.Minimize` or `Maximize`, or a string "min" or "max".
:param abstols: if defined, accepts either a number or a list of numbers having the same size as the `exprs` argument,
interpreted as absolute tolerances. If passed asingle number, this tolerance number will be used for all
passes.
:param reltols: if defined, accepts either a number or a list of numbers having the same size as the `exprs` argument,
interpreted as absolute tolerances. If passed asingle number, this tolerance number will be used for all
passes.
Note:
tolerances are used at each step to constraint the previous
objective value to be be 'no worse' than the value found in the
last pass. For example, if relative tolerance is 2% and pass #1 has
found an objective of 100, then pass #2 will constraint the first
goal to be no greater than 102 if minimizing, or
no less than 98, if maximizing.
If one pass fails, return the previous pass' solution. If the solve fails at the first
goal, then return None.
Return:
If successful, returns a tuple with all pass solutions, reversed else None.
The current solution of the model is the first solution in the tuple.
"""
if not goals:
self.error("solve_with_goals requires a non-empty list of goals, got: {0!r}".format(goals))
return None
if not is_indexable(goals):
self.fatal("solve_with_goals requires an indexable sequence of goals, got: {0!s}", goals)
is_verbose = kwargs.pop('verbose', False)
nb_goals =len(goals)
actual_goal_names = ["goal%d" % (gi + 1) for gi in range(nb_goals)]
if isinstance(goal_names, list):
for i in range(nb_goals):
try:
gn = goal_names[i]
if gn:
actual_goal_names[i] = gn
except (KeyError, TypeError, ValueError):
pass
actual_goals = [(gn, self._lfactory._to_expr(g)) for gn, g in zip(actual_goal_names, goals)]
# --- senses ---
abstols_ = []
reltols_ = []
# compile tolerances to abstols, reltols
if abstols is not None:
abstols_ = self._typecheck_optional_num_seq(abstols, expected_size=nb_goals, caller='Model.solve_with_goals')
if reltols is not None:
reltols_ = self._typecheck_optional_num_seq(reltols, expected_size=nb_goals, caller='Model.solve_with_goals')
if not abstols_:
abstols_ = [1e-6] * nb_goals
if not reltols_:
reltols_ = [1e-4] * nb_goals
old_objective_expr = self._objective_expr
old_objective_sense = self._objective_sense
pass_count = 0
m = self
results = []
if not is_iterable(senses, accept_string=False):
senses = generate_constant(ObjectiveSense.parse(senses), count_max=nb_goals)
def lex_info(msg):
if is_verbose:
print("-- lex_goals: {0}".format(msg))
# keep extra constraints, in order to remove them at the end.
extra_cts = []
cplex_param_key = Context.cplex_parameters_key
ctx_params = kwargs.get(cplex_param_key)
iter_pass_params = generate_constant(None, nb_goals)
baseline_params = None # parameters to restore at each iteration, default is to do nothing
if ctx_params and is_iterable(ctx_params):
# must pop out the list as normal solve won't have it.
pass_params = list(kwargs.pop(cplex_param_key))
if len(pass_params) != nb_goals:
self.fatal("List of parameters should have same length as goals, expecting: {0} but a list of size {1} was passed",
nb_goals, pass_params)
else:
iter_pass_params = iter(pass_params)
baseline_params = m.context.parameters # need to clear/reset parameters at each pass
# --- main loop ---
prev_step = (None, None, None)
all_solutions = []
solve_kwargs = kwargs.copy()
current_sol = None
try:
for (goal_name, goal_expr), next_sense, abstol, reltol in zip(actual_goals, senses, abstols_, reltols_):
if goal_expr.is_constant() and pass_count > 1:
self.warning("Constant expression in lexicographic solve: {0!s}, skipped", goal_expr)
continue
pass_count += 1
if pass_count > 1:
prev_goal, prev_obj, prev_sense = prev_step
tolerance = compute_tolerance(prev_obj, abstol, reltol)
if prev_sense.is_minimize():
pass_ct = m._post_constraint(prev_goal <= prev_obj + tolerance)
else:
pass_ct = m._post_constraint(prev_goal >= prev_obj - tolerance)
pass_ct.name = "lex_{0}_ct".format(pass_count)
lex_info("pass #{0} generated constraint with rhs: {1}, tolerance={2:.3g}"
.format(pass_count, str(pass_ct.rhs), tolerance))
extra_cts.append(pass_ct)
sense = self._resolve_sense(next_sense)
lex_info("starting pass %d, %s: %s" % (pass_count, sense.verb, str_maxed(goal_expr, 64)))
m.set_objective(sense, goal_expr)
if write_pass_files: # pragma: no cover
pass_basename = f"lex_{self.name}_{goal_name}_pass{pass_count}"
dump_path = self.export_as_lp(path='.', basename=pass_basename)
lex_info("saved pass file: {0}".format(dump_path))
# --- update pass parameters, if any
pass_param = next(iter_pass_params)
if pass_param:
# print('applying custom parameters')
# pass_param.print_information()
m.context.update_cplex_parameters(pass_param)
m.context.cplex_parameters.print_information()
# ---
if current_sol and pass_count > 1:
solve_kwargs['lex_mipstart'] = current_sol
current_sol = m.solve(**solve_kwargs)
# restore params if need be
if baseline_params:
m.context.cplex_parameters = baseline_params
if current_sol is not None:
current_sol.set_name("lex_{0}_{1}_{2}".format(self.name, goal_name, pass_count))
current_obj = current_sol.objective_value
results.append(current_obj)
prev_step = (goal_expr, current_obj, sense)
all_solutions.append(current_sol)
lex_info("objective value for pass #{0} is: {1}".format(pass_count, current_sol.objective_value))
if write_pass_files:
pass_basename = f"lex_{self.name}_{goal_name}_pass{pass_count}"
if self.problem_type == 'LP':
bas_path = self.export_basis(path='.', basename=pass_basename)
lex_info("saved pass basis file: {0}".format(bas_path))
else:
write_level = kwargs.get('write_level', 'auto')
mst_path = current_sol.export_as_mst(path='.', basename=pass_basename, write_level=write_level)
lex_info("saved pass MST file: {0}".format(mst_path))
if solution_callbackfn:
solution_callbackfn(current_sol)
else: # pragma: no cover
sd = m.solve_details
status = sd.status
self.error("lexicographic: pass {0} fails, status={1} ({2}), stopping",
pass_count, status, sd.status_code)
break
finally:
# print("-> start restoring model at end of lexicographic")
while extra_cts:
# using LIFO logic to avoid holes in indices.
ct_to_remove = extra_cts.pop()
# print("* removing constraint: name: {0}, idx: {1}".format(ct_to_remove.name, ct_to_remove.index))
self._remove_constraint_internal(ct_to_remove)
# restore objective whatsove
self.set_objective(old_objective_sense, old_objective_expr)
# print("<- end restoring model at end of lexicographic")
# return a solution or None
return tuple(reversed(all_solutions))
def _has_solution(self):
# INTERNAL
return self._solution is not None
def _set_solution(self, new_solution):
"""
INTERNAL: Sets this solution as the model's current solution.
Copies values to variables (for now, let's think more about this)
:param new_solution:
:return:
"""
self._solution = new_solution
def _check_has_solution(self):
# see if we can refine messages here...
if self._solution is None:
if self._solve_details is None:
self.fatal("Model<{0}> has not been solved yet", self.name)
else:
self.fatal("Model<{0}> did not solve successfully", self.name)
def _check_solved_as_mip(self, caller, do_raise):
if self._check_mip_for_mipstarts:
if not self._solved_as_mip():
msg = "{0} is only available for MIP problems".format(caller)
if do_raise:
self.fatal(msg)
else:
self.error(msg)
return False
# either a MIP or we don't care...
return True
[docs] def add_mip_start(self, mip_start_sol, effort_level=None, write_level=None, complete_vars=False, eps_zero=1e-6):
""" Adds a (possibly partial) solution to use as a starting point for a MIP.
This is valid only for models with binary or integer decision variables.
The given solution must contain the value for at least one binary or integer variable.
This feature is also known as 'warm start'.
The solution passed in input is copied into a new instance of :class:`docplex.mp.SolveSolution`.
Depending on the "write_level" argument, some filtering operations can be performed: by default (no
explicit write level) only discrete variables are copied. When an explicit level is passed,
the level controls whether zero values are passed ore not: for example "WiteLevel.NonZeroDiscreteVars" specifies
only copying non zero values for discrete variables.
Args:
mip_start_sol (:class:`docplex.mp.solution.SolveSolution`): The solution object to use as a starting point.
write_level: an optional enumerated value from class :class:`docplex.mp.constants.WriteLevel`, controlling
which variables are copied to the MIP start solution. By default, only discrete variables are copied.
complete_vars: optinal flag. If False (default), only variables mentioned in the solution are copied. If True,
all variables in the model are copied to the MIP start.
effort_level: an optional enumerated value of class :class:`docplex.mp.constants.EffortLevel`, or None.
Returns:
an instance of :class:`doplex.mp.SolveSolution`, different from the one passed in input if the conversion succeeds,
else None (typically for LP models).
Examples:
The default values correspond to copying only variables explicitly mentioned in the passed solution,
copying only discrete variables, including zeros.
To exclude zeros, use
>>> mdl.add_mip_start(sol, write_level=WriteLevel.NonZeroDiscreteVars)
To include all variables in the model, wincluding continuous ones, use:
>>> mdl.add_mip_start(sol, write_level=WriteLevel.AllVars, complete_vars=True)
See Also:
:class:`docplex.mp.constants.EffortLevel`
:class:`docplex.mp.constants.WriteLevel`
:class:`docplex.mp.solution.SolveSolution`
"""
assert eps_zero >= 0
assert eps_zero < 1
mip_start_ = None
if self._check_solved_as_mip(caller="Model.add_mip_start", do_raise=False):
try:
mip_start_ = mip_start_sol.as_mip_start(write_level=write_level,
complete_vars=bool(complete_vars),
eps_zero=eps_zero)
mip_start_.check_as_mip_start()
effort = EffortLevel.parse(effort_level)
self._mipstarts.append((mip_start_, effort))
except AttributeError:
self.fatal("add_mip_starts expects solution, {0!r} was passed", mip_start_sol)
return mip_start_
@property
def mip_starts(self):
""" This property returns the list of MIP start solutions (a list of instances of :class:`docplex.mp.solution.SolveSolution`)
attached to the model if MIP starts have been defined, possibly an empty list.
"""
warnings.warn("Model.mip_starts is deprecated. Use Model.iter_mip_starts instead"
, DeprecationWarning, stacklevel=2)
return [s for (s, _) in self.iter_mip_starts()]
@property
def number_of_mip_starts(self):
""" This property returns the number of MIP start associated with the model.
*New in version 2.10*
"""
return len(self._mipstarts)
[docs] def iter_mip_starts(self):
""" This property returns an iterator on the MIP starts associated with
the model.
It returns tuples of size 2:
- first element is a solution (an instance of :class:`docplex.mp.solution.SolveSolution`)
- second is an enumerated value of type :class:`docplex.mp.constants.EffortLevel`
*New in version 2.10*
"""
return iter(self._mipstarts)
[docs] def clear_mip_starts(self):
""" Clears all MIP starts associated with the model.
Note: this clears only MIP starts provided by the user via the `Model.add_mip_start` method.
This does not remove interbal solutions found by previous solves. To run a fresh solve,
\and forget all about previous solves, use the `clean_before_solve=True` keyword argument for
:func:`solv()`
See Also:
:func:`add_mip_start`
:func:`solve`
"""
self._mipstarts = []
[docs] def read_mip_starts(self, mst_path):
""" Read MIP starts from a file.
Reads the file and returns a list of (solution, effort_level) tuples.
:param mst_path: the path to mip start file (in CPLEX MST file format)
:return: a list of tuples of size 2; the first element is an instance of `SolveSolution`
and the second element is an enumerated value of type `EffortLevel`
See Also:
:class:`docplex.mp.constants.EffortLevel`
:class:`docplex.mp.solution.SolveSolution`
* New in version 2.10*
"""
self._check_solved_as_mip(caller="Model.read_mip_starts", do_raise=True)
from docplex.mp.sol_xml_reader import read_mst_file
self.info("Reading mip starts from file: {0}".format(mst_path))
mip_starts = read_mst_file(mst_path, self, caller='Model.read_mip_starts')
if mip_starts is not None:
if not mip_starts:
self.warning("Found no MIP starts in file: {0}", mst_path)
else:
self.info("Read {0} MIP starts in file: {1}".format(len(mip_starts), mst_path))
self._mipstarts = mip_starts
return mip_starts
else:
# do not overwrite current mip starts (IMHO)
return None
[docs] def set_lp_start_basis(self, dvar_stats, lct_stats):
""" Provides an initial basis for a LP problem.
:param dvar_stats: an ordered sequence (list) of basis status objects, one for
each decision variable in the model.
:param lct_stats: an ordered sequence (list) of basis status objects, one for each linear constraint
in the model
Note:
Basis status are values of the enumerated type :class:`docplex.mp.constants.BasisStatus`.
See Also:
:class:`docplex.mp.constants.BasisStatus`.
* New in version 2.10*
"""
l_dvar_stats = StaticTypeChecker.typecheck_initial_lp_stats\
(logger=self, stats=dvar_stats, stat_type='variable', caller='Model.set_lp_start_basis')
l_lct_stats = StaticTypeChecker.typecheck_initial_lp_stats\
(logger=self, stats=lct_stats, stat_type='constraint', caller='Model.set_lp_start_basis')
self.__engine.set_lp_start(l_dvar_stats, l_lct_stats)
@property
def objective_value(self):
""" This property returns the value of the objective expression in the solution of the last solve.
In case of a multi-objective, only the value of the first objective is returned
Raises an exception if the model has not been solved successfully.
"""
self._check_has_solution()
return self._objective_value()
def _objective_value(self):
return self.solution.objective_value
@property
def multi_objective_values(self):
""" This property returns the list of values of the objective expressions in the solution of the last solve.
Raises an exception if the model has not been solved successfully.
*New in version 2.9*
"""
self._check_has_solution()
return self._multi_objective_values()
def _multi_objective_values(self):
# INTERNAL
return self.solution.multi_objective_values
@property
def blended_objective_values(self):
""" This property returns the list of values of the blended objective expressions based on the decreasing
order of priorities in the solution of the last solve.
Raises an exception if the model has not been solved successfully.
*New in version 2.9.*
"""
self._check_has_solution()
blended_obj_values = self.solution.get_blended_objective_value_by_priority()
return blended_obj_values
def _reported_objective_value(self, failure_obj=0):
return self.solution.objective_value if self.solution else failure_obj
def _resolve_path(self, path_arg, basename_arg, extension):
# INTERNAL
if is_string(path_arg):
if os.path.isdir(path_arg):
if path_arg == ".":
path_arg = os.getcwd()
return self._make_output_path(extension, basename_arg, path_arg)
else:
# add extension if not present (but not twice!)
return path_arg if path_arg.endswith(extension) else path_arg + extension
else:
assert path_arg is None
return self._make_output_path(extension, basename_arg, path_arg)
def _make_output_path(self, extension, basename, path=None):
return make_output_path2(self.name, extension, basename, path)
def _get_printer(self, format_spec, do_raise=False, silent=False):
# INTERNAL
printer_kwargs = {'full_obj': self._print_full_obj}
format_ = parse_format(format_spec)
printer = None
if format_.name == 'LP':
printer = LPModelPrinter(**printer_kwargs)
else:
if do_raise:
self.fatal("Unsupported output format: {0!s}", format_spec)
elif not silent:
self.error("Unsupported output format: {0!s}", format_spec)
return printer
def dump_as_lp(self, path=None, basename=None):
return self._export_from_cplex(path, basename, format_spec="lp")
[docs] def export_as_lp(self, path=None, basename=None, hide_user_names=False):
""" Exports a model in LP format.
Args:
basename: Controls the basename with which the model is printed.
Accepts None, a plain string, or a string format.
if None, uses the model's name;
if passed a plain string, the string is used in place of the model's name;
if passed a string format (either with %s or {0}, it is used to format the
model name to produce the basename of the written file.
path: A path to write file, expects a string path or None.
can be either a directory, in which case the basename
that was computed with the basename argument, is appended to the directory to produce
the file.
If given a full path, the path is directly used to write the file, and
the basename argument is not used.
If passed None, the output directory will be ``tempfile.gettempdir()``.
hide_user_names: A Boolean indicating whether or not to keep user names for
variables and constraints. If True, all names are replaced by `x1`, `x2`, ... for variables,
and `c1`, `c2`, ... for constraints.
Returns:
The full path of the generated file, or None if an error occured.
Examples:
Assuming the model's name is `mymodel`:
>>> m.export_as_lp()
will write ``mymodel.lp`` in ``gettempdir()``.
>>> m.export_as_lp(basename="foo")
will write ``foo.lp`` in ``gettempdir()``.
>>> m.export_as_lp(basename="foo", path="e:/home/docplex")
will write file ``e:/home/docplex/foo.lp``.
>>> m.export_as_lp("e/home/docplex/bar.lp")
will write file ``e:/home/docplex/bar.lp``.
>>> m.export_as_lp(basename="docplex_%s", path="e/home/")
will write file ``e:/home/docplex/docplex_mymodel.lp``.
"""
return self.export(path, basename, hide_user_names=hide_user_names, format_spec='lp')
[docs] def export_as_sav(self, path=None, basename=None):
""" Exports a model in CPLEX SAV format.
Exporting to SAV format requires that CPLEX is installed and
available in PYTHONPATH. If the CPLEX runtime cannot be found, an exception is raised.
Args:
basename: Controls the basename with which the model is printed.
Accepts None, a plain string, or a string format.
If None, the model's name is used.
If passed a plain string, the string is used in place of the model's name.
If passed a string format (either with %s or {0}), it is used to format the
model name to produce the basename of the written file.
path: A path to write the file, expects a string path or None.
Can be a directory, in which case the basename
that was computed with the basename argument, is appended to the directory to produce
the file.
If given a full path, the path is directly used to write the file, and
the basename argument is not used.
If passed None, the output directory will be ``tempfile.gettempdir()``.
Returns:
The full path of the generated file, or None if an error occured.
Examples:
See the documentation of :func:`export_as_lp` for examples of pathname generation.
The logic is identical for both methods.
"""
return self._export_from_cplex(path, basename, format_spec="sav")
dump_as_sav = export_as_sav
[docs] def export_as_mps(self, path=None, basename=None):
""" Exports a model in MPS format.
Exporting to MPS format requires that CPLEX is installed and
available in PYTHONPATH. If the CPLEX runtime cannot be found, an exception is raised.
Args:
basename: Controls the basename with which the model is printed.
Accepts None, a plain string, or a string format.
If None, the model's name is used.
If passed a plain string, the string is used in place of the model's name.
If passed a string format (either with %s or {0}), it is used to format the
model name to produce the basename of the written file.
path: A path to write the file, expects a string path or None.
Can be a directory, in which case the basename
that was computed with the basename argument, is appended to the directory to produce
the file.
If given a full path, the path is directly used to write the file, and
the basename argument is not used.
If passed None, the output directory will be ``tempfile.gettempdir()``.
Returns:
The full path of the generated file, or None if an error occured.
Examples:
See the documentation of :func:`export_as_lp` for examples of pathname generation.
The logic is identical for both methods.
"""
return self._export_from_cplex(path, basename, format_spec="mps")
[docs] def export_as_savgz(self, path=None, basename=None):
""" Exports a model in compressed SAV format.
Exporting to SAV compressed format requires that CPLEX is installed and
available in PYTHONPATH. If the CPLEX runtime cannot be found, an exception is raised.
Arguments 'path' and 'basename' have similar usage as for :func:`export_as_lp`.
Returns:
The full path of the generated file, or None if an error occured.
Examples:
See the documentation of :func:`export_as_lp` for examples of pathname generation.
The logic is identical for both methods.
*New In 2.19*
"""
return self._export_from_cplex(path, basename, format_spec="sav.gz")
def _export_from_cplex(self, path=None, basename=None, hide_user_names=False,
format_spec="lp"):
return self._export(path, basename,
use_engine=True,
hide_user_names=hide_user_names,
format_spec=format_spec)
def export(self, path=None, basename=None,
hide_user_names=False, format_spec="lp"):
# INTERNAL
return self._export(path, basename,
use_engine=False,
hide_user_names=hide_user_names,
format_spec=format_spec)
def _export(self, path=None, basename=None,
use_engine=False, hide_user_names=False,
format_spec="lp"):
# INTERNAL
# path is either a nonempty path string or None
self._checker.typecheck_string(path, accept_none=True, accept_empty=False)
self._checker.typecheck_string(basename, accept_none=True, accept_empty=False)
# INTERNAL
_format = parse_format(format_spec)
if not _format:
self.fatal("Not a supported exchange format: {0!s}", format_spec)
extension = _format.extension
# combination of path/directory and basename resolution are done in resolve_path
path = self._resolve_path(path, basename, extension)
ret = self._export_to_path(path, hide_user_names, use_engine, _format)
if ret:
self.trace("model file: {0} overwritten", path)
return ret
def _export_to_path(self, path, hide_user_names=False, use_engine=False, format_spec="lp"):
# INTERNAL
format_ = parse_format(format_spec)
try:
if use_engine:
# rely on engine for the dump
if self.has_cplex():
self.__engine.export(path, format_)
else: # pragma: no cover
self.fatal(
"Exporting to {0} requires CPLEX, but a local CPLEX installation could not be found, file: {1} could not be written",
format_.name, path)
return None
else:
# a path is not a stream but anyway it will work
self._export_to_stream(stream=path, hide_user_names=hide_user_names, format_spec=format_)
return path
except IOError:
self.error("Cannot open file: \"{0}\", model: {1} not exported".format(path, self.name))
raise
def _export_to_stream(self, stream, hide_user_names=False, format_spec="lp"):
format_ = parse_format(format_spec)
printer = self._get_printer(format_, do_raise=False, silent=True)
if printer:
printer.set_mangle_names(hide_user_names)
printer.printModel(self, stream)
else:
self.__engine.export(stream, format_spec)
[docs] def export_to_stream(self, stream, hide_user_names=False, format_spec="lp"):
""" Export the model to an output stream in LP format.
A stream can be one of:
- a string, interpreted as a system path,
- None, interpreted as `stdout`, or
- a Python file-type object (e.g. a StringIO() instance).
Args:
stream: An object defining where the output will be sent.
hide_user_names: An optional Boolean indicating whether or not to keep user names for
variables and constraints. If True, all names are replaced by `x1`, `x2`, ... for variables,
and `c1`, `c2`, ,... for constraints. Default is to keep user names.
"""
self._export_to_stream(stream, hide_user_names, format_spec)
[docs] def export_as_lp_string(self, hide_user_names=False):
""" Exports the model to a string in LP format.
The output string contains the model in LP format.
Args:
hide_user_names: An optional Boolean indicating whether or not to keep user names for
variables and constraints. If True, all names are replaced by `x1`, `x2`, ... for variables,
and `c1`, `c2`, ... for constraints. Default is to keep user names.
Returns:
A string, containing the model exported in LP format.
"""
return self.export_to_string(hide_user_names, "lp")
@property
def lp_string(self):
""" This property returns a string encoding the model in LP format.
*New in version 2.16*
"""
return self.export_as_lp_string()
[docs] def export_as_mps_string(self):
""" Exports the model to a string in MPS format.
Returns:
A string, containing the model exported in MPS format.
*New in version 2.13*
"""
return self._export_as_cplex_string("mps")
[docs] def export_as_sav_string(self):
""" Exports the model to a string of bytes in SAV format.
Returns:
A string of bytes..
*New in version 2.13*
"""
return self._export_as_cplex_string("sav")
def _export_as_cplex_string(self, format_spec):
# INTERNAL
_format = parse_format(format_spec)
if not self.has_cplex():
self.fatal("Exporting to {0} requires CPLEX, but a local CPLEX installation could not be found",
_format.name)
from io import BytesIO
bs = BytesIO()
self.__engine.export(bs, _format)
raw_res = bs.getvalue()
if _format.is_binary:
# for b, by in enumerate(raw_res):
# nl = (b % 21 == 20)
# print(f" {by}", end='\n' if nl else '')
# print()
return raw_res
else:
return raw_res.decode(self.parameters.read.fileencoding.get())
def export_to_string(self, hide_user_names=False, format_spec="lp"):
# INTERNAL
oss = StringIO()
self._export_to_stream(oss, hide_user_names, format_spec)
return oss.getvalue()
def export_parameters_as_prm(self, path=None, basename=None):
# path is either a nonempty path string or None
self._checker.typecheck_string(path, accept_none=True, accept_empty=False)
self._checker.typecheck_string(basename, accept_none=True, accept_empty=False)
# combination of path/directory and basename resolution are done in resolve_path
prm_path = self._resolve_path(path, basename, extension='.prm')
self.parameters.export_prm_to_path(path=prm_path)
return prm_path
def export_parameters_as_ops(self, path=None, basename=None):
# path is either a nonempty path string or None
self._checker.typecheck_string(path, accept_none=True, accept_empty=False)
self._checker.typecheck_string(basename, accept_none=True, accept_empty=False)
# combination of path/directory and basename resolution are done in resolve_path
ops_path = self._resolve_path(path, basename, extension='.ops')
self.parameters.export_as_ops_file(filename=ops_path)
return ops_path
def export_annotations(self, path=None, basename=None):
from docplex.mp.anno import ModelAnnotationPrinter
self._checker.typecheck_string(path, accept_none=True, accept_empty=False)
self._checker.typecheck_string(basename, accept_none=True, accept_empty=False)
# combination of path/directory and basename resolution are done in resolve_path
anno_path = self._resolve_path(path, basename, extension='.ann')
ap = ModelAnnotationPrinter()
ap.print_to_stream(self, anno_path)
return anno_path
def _check_problem_type(self, feature, requires_solution=True, accept_qxp=True):
if self._solve_details is None:
self.fatal('{0} are not available, model is not solved yet'.format(feature))
elif requires_solution and self._solution is None:
self.fatal('{0} require a solution, but model is not solved with a solution'.format(feature))
elif self._solved_as_lp():
pass
elif not accept_qxp and self.is_quadratic():
self.fatal('{0} are not available for QP/QCP problems'.format(feature))
elif self._solved_as_mip():
self.fatal('{0} are not available for integer problems'.format(feature))
def _dual_value1(self, linear_ct):
# PRIVATE
self._check_problem_type(feature='dual values')
self._checker.typecheck_ct_added_to_model(self, linear_ct)
dvs = self._dual_values([linear_ct])
return dvs[0]
[docs] def dual_values(self, cts):
""" Returns the dual values of a sequence of linear constraints.
Note: the model must a pure LP: no integer or binary variable, no piecewise, no SOS.
The model must also be solved successfully before calling this method.
:param cts: a sequence of linear constraints.
:return: a sequence of float numbers
"""
self._check_problem_type(feature='dual values')
checked_lcts = self._checker.typecheck_constraint_seq(cts, check_linear=True)
return self._dual_values(checked_lcts)
def _dual_values(self, cts):
# PRIVATE
checked_lcts = self._checker.typecheck_cts_added_to_model(mdl=self, cts=cts)
sol = self.solution
sol.ensure_dual_values(self, self.get_engine())
return sol.get_dual_values(checked_lcts)
def _slack_value1(self, ct):
# private
self._checker.typecheck_ct_added_to_model(mdl=self, ct=ct)
self._check_has_solution()
return self._slack_values([ct])[0]
[docs] def slack_values(self, cts):
""" Return the slack values for a sequence of constraints.
Slack values are available for linear, quadratic and indicator constraints.
The model must be solved successfully before calling this method.
:param cts: a sequence of constraints.
:return: a list of float values, in the same order as the constraints.
"""
self._check_has_solution()
ckr = self._checker
cts1 = ckr.typecheck_constraint_seq(cts)
cts2 = ckr.typecheck_cts_added_to_model(self, cts1)
return self._slack_values(cts2)
def _slack_values(self, cts):
checked_cts = self._checker.typecheck_constraint_seq(cts)
# ---
sol = self.solution
sol.ensure_slack_values(self, self.get_engine())
return sol.get_slacks(checked_cts)
def _reduced_cost1(self, dvar):
# PRIVATE
self._check_problem_type(feature='reduced costs')
#self._checker.typecheck_var(dvar)
rcs = self._reduced_costs([dvar])
return rcs[0]
[docs] def get_cuts(self):
""" Returns the number of cuts under the form of a dict(type -> number).
Note: The model must also be solved successfully before calling this method.
:return: the number of cuts under the form of a dict(type -> number).
"""
sol = self.solution
assert sol is not None
return sol.get_cuts()
[docs] def get_num_cuts(self, cut_type):
""" Returns the number of cuts for a specific type.
Note: The model must also be solved successfully before calling this method.
:param cut_type: a cut type.
:return: the number of cuts associated to this type of cut.
"""
sol = self.solution
assert sol is not None
return sol.get_num_cuts(cut_type)
[docs] def reduced_costs(self, dvars):
""" Returns the reduced costs for a variable iterable.
Note: the model must a pure LP: no integer or binary variable, no piecewise, no SOS.
The model must also be solved successfully before calling this method.
:param dvars: a sequence of variables.
:return: a list of float numbers, in the same order as the variable sequence.
"""
self._check_problem_type(feature='reduced costs')
checked_vars = self._checker.typecheck_var_seq(dvars, caller='Model.reduced_costs')
return self._reduced_costs(checked_vars)
def _reduced_costs(self, dvars):
sol = self.solution
assert sol is not None
sol.ensure_reduced_costs(model=self, engine= self.get_engine())
return sol.get_reduced_costs(dvars)
[docs] def quadratic_dual_slacks(self, *args):
""" Returns quadratic dual slacks as a dict of dicts.
Can be called in two forms: either with no arguments, in which case it returns
quadratic dual slacks for all quadratic constraints in the model, or with
a list of quadratic constraints. In this case it returns only quadratic dual slacks for those constraints
:param args: accepts either no arguments,or a list of quadratic constraints.
:return: a Python dictionary, whose keys are quadratic constraints, and
values are dictionaries from variables to quadratic dual slacks.
*New in version 2.15*
"""
nb_args = len(args)
if 0 == nb_args:
qcts = self.iter_quadratic_constraints()
elif 1 == nb_args:
qcts = args[0]
else:
qcts = None
self.fatal("Model.quadratic_dual_slacks expects either an iteratble on quadratic constraints, or no args.")
self._check_problem_type('quadratic_dual_slacks', requires_solution=True, accept_qxp=True)
cpx = self._get_cplex(do_raise=True, msgfn=lambda: "Quadratic dual slacks require CPLEX library")
checked_qcts = self._checker.typecheck_quadratic_constraint_seq(qcts)
if checked_qcts:
qixs = [qc.index for qc in checked_qcts]
cpx_qdss = cpx.solution.get_quadratic_dualslack(qixs)
qds_as_dict = {checked_qcts[q]: {self._var_by_index(idx): qds for idx, qds in zip(cpx_sp.ind, cpx_sp.val)} \
for q, cpx_sp in enumerate(cpx_qdss) }
return qds_as_dict
else:
return {}
def _var_basis_status1(self, dvar):
# internal
return self.var_basis_statuses([dvar])[0]
[docs] def var_basis_statuses(self, dvars):
"""
Returns basis status for a batch of variables.
:param dvars: an iterable returning variables.
:return: a list of basis status, of type :class:`docplex.mp.constants.BasisStatus`.
The order of the list is the order in which variables were returned by the iterable.
*New in version 2.10*
"""
self._check_problem_type(feature='basis status', requires_solution=False, accept_qxp=False)
checked_vars = self._checker.typecheck_var_seq(dvars)
return self._var_basis_status(checked_vars)
def _var_basis_status(self, dvars):
return self._generic_get_basis_status(dvars, pos=0,
sol_getter=lambda s_, dvs_: s_.get_var_basis_statuses(dvs_))
[docs] def linear_constraint_basis_statuses(self, lcts):
"""
Returns basis status for a batch of linear constraints.
:param lcts: an iterable returning linear constraints.
:return: a list of basis status, of type :class:`docplex.mp.constants.BasisStatus`.
The order of the list is the order in which constraints were returned by the iterable.
*New in version 2.10*
"""
self._check_problem_type(feature='basis status', requires_solution=False, accept_qxp=False)
checked_lincts = self._checker.typecheck_constraint_seq(lcts, check_linear=True, accept_range=True)
return self._linearct_basis_status(checked_lincts)
def _linearct_basis_status(self, lcts):
return self._generic_get_basis_status\
(lcts, pos=1, sol_getter=lambda s_, cts_: s_.get_linearct_basis_statuses(cts_))
def _generic_get_basis_status(self, objs, pos, sol_getter):
assert pos in {0, 1}
sol = self.solution
if sol:
sol.ensure_basis_statuses(model=self, engine= self.get_engine())
return sol_getter(sol, objs) #sol.get_linearct_basis_statuses(lcts)
else:
basis_tuple = self.__engine.get_basis(self)
basis = basis_tuple[pos]
if not len(basis):
self.error("No basis is available")
return [BasisStatus.NotABasisStatus] * len(objs)
else:
return [ BasisStatus.parse(basis.get(obj, -1)) for obj in objs]
[docs] def has_basis(self):
""" returns True if the model contains basis information.
*New in version 2.9*
"""
sol = self.solution
if sol:
sol.ensure_basis_statuses(model=self, engine= self.get_engine())
return sol._has_basis()
else:
var_basis, linct_basis = self.__engine.get_basis(self)
return len(var_basis) > 0
def _write_cplex_file(self, name, path, basename, extension, cpx_write_fn, check_fn=lambda m_: 0):
check_fn(self)
export_basename = normalize_basename(self.name, force_lowercase=True)
export_path = make_output_path2(actual_name=export_basename,
extension=extension,
path=path,
basename_fmt=basename)
if export_path:
msg = "CPLEX runtime is required for {0} export - file {1} not written".format(name, export_path)
cpx = self._get_cplex(do_raise=True, msgfn=lambda: msg)
try:
cpx_write_fn(cpx, export_path)
except Exception as ex:
print(f"An error occured: '{str(ex)}' -- write aborted")
return None
return export_path
def _check_basis(self):
if not self.has_basis():
self.fatal("No basis data is available for model '{0}'- cannot write basis file",
self.name)
def export_basis(self, path=None, basename=None):
return self._write_cplex_file(name='basis', path=path, basename=basename,
extension='.bas',
cpx_write_fn=lambda cpx_, path_: cpx_.solution.basis.write(path_),
check_fn=lambda m_: m_._check_basis())
DEFAULT_VAR_VALUE_QUOTED_SOLUTION_FMT = ' \"{varname}\"={value:.{prec}f}'
DEFAULT_VAR_VALUE_UNQUOTED_SOLUTION_FMT = ' {varname}={value:.{prec}f}'
DEFAULT_OBJECTIVE_FMT = "{0}: {1:.{prec}f}"
@classmethod
def supports_logical_constraints(cls):
return cls()._supports_logical_constraints()
def _supports_logical_constraints(self):
# INTERNAL
ok, _ = self.__engine.supports_logical_constraints()
return ok
_is_cplex_ce = None
@classmethod
def is_cplex_ce(cls):
if cls._is_cplex_ce is None:
m = Model()
if not m.has_cplex():
cls._is_cplex_ce = False
else:
try:
for i in range(1001):
v = m.integer_var()
m.add_constraint(v <= i)
m.solve()
cls._is_cplex_ce = False
except DOcplexLimitsExceeded as e:
cls._is_cplex_ce = True
return cls._is_cplex_ce
def _check_logical_constraint_support(self):
ok, why = self.__engine.supports_logical_constraints()
if not ok:
assert why
self.fatal(msg=why)
@classmethod
def is_docplex_debug(cls):
return not not os.environ.get("DOCPLEX_DEBUG")
def _has_username_with_spaces(self):
for v in self.iter_variables():
if v.has_user_name() and ' ' in v.name:
return True
return False
[docs] def print_solution(self, print_zeros=False,
solution_header_fmt=None,
var_value_fmt=None,
**kwargs):
""" Prints the values of the model variables after a solve.
Only valid after a successful solve. If the model has not been solved successfully, an
exception is raised.
Args:
print_zeros (Boolean): If False, only non-zero values are printed. Default is False.
solution_header_fmt: a solution header string in format syntax, or None.
This format will be passed to :func:`docplex.mp.solution.SolveSolution.display`.
var_value_fmt : A format string to format the variable name and value. Again, the default uses the automatically computed precision.
See also:
:func:`docplex.mp.solution.SolveSolution.display`
"""
self._check_has_solution()
if var_value_fmt is None:
if self._has_username_with_spaces():
var_value_fmt = self.DEFAULT_VAR_VALUE_QUOTED_SOLUTION_FMT
else:
var_value_fmt = self.DEFAULT_VAR_VALUE_UNQUOTED_SOLUTION_FMT
if not self.has_objective():
var_value_fmt = var_value_fmt[2:]
# scope of variables.
iter_vars = self.iter_variables() if print_zeros else None
# if some username has a whitespace, use quoted format
self.solution.display(print_zeros=print_zeros,
header_fmt=solution_header_fmt,
value_fmt=var_value_fmt,
iter_vars=iter_vars, **kwargs)
[docs] def report(self):
""" Prints the value of the objective and the KPIs.
Only valid after a successful solve, otherwise states that the model is not solved.
"""
if self._has_solution():
if self.has_multi_objective():
mobj_values = self._multi_objective_values()
prec = self._float_precision
s_mobjs = ", ".join("{0:.{prec}f}".format(mo, prec=prec) for mo in mobj_values)
print("* model {0} solved with objectives = [{1}]".format(self.name, s_mobjs))
else:
used_prec = self._float_precision
print("* model {0} solved with objective = {1:.{prec}f}".format(self.name,
self._objective_value(), prec=used_prec))
self.report_kpis()
else:
status = self.solve_details.status
self.info("Model {0} has not been solved successfully, status is: {1}.".format(self.name, status))
[docs] def report_kpis(self, solution=None, selected_kpis=None, kpi_format='* KPI: {1:<{0}} = '):
""" Prints the values of the KPIs.
KPIs require a solution to be evaluated. This solution can be passed explicitly as a parameter,
or the model is assumed to be solved with a valid solution.
:param solution: an instance of `SolveSolution`. If not passed, the model solution is
queried. If the model has no solution, an exception is raised.
:param selected_kpis: an optional iterable returning the KPIs to print.
The default behavior is to print all kpis.
:param kpi_format: an optional format to print the KPi name and its value.
See Also:
:class:`docplex.mp.solution.SolveSolution`
:func:`new_solution`
"""
kpi_num_format = kpi_format + self._float_meta_format % (2,)
kpi_str_format = kpi_format + '{2!s}'
printed_kpis = list(selected_kpis if is_iterable(selected_kpis) else self.iter_kpis())
try:
max_kpi_name_len = max(len(k.name) for k in printed_kpis) # max() raises ValueError on empty
except ValueError:
max_kpi_name_len = 0
for kpi in printed_kpis:
kpi_value = kpi.compute(solution)
if is_number(kpi_value):
k_format = kpi_num_format
else:
k_format = kpi_str_format
if type(k_format) != type(kpi.name):
# infamous mix of str and unicode. Should happen only
# in py2. Let's convert things
if isinstance(k_format, str):
k_format = k_format.decode('utf-8')
else:
k_format = k_format.encode('utf-8')
output = k_format.format(max_kpi_name_len, kpi.name, kpi_value)
try:
print(output)
except UnicodeEncodeError:
encoding = sys.stdout.encoding if sys.stdout.encoding else 'ascii'
print(output.encode(encoding,
errors='backslashreplace'))
[docs] def kpis_as_dict(self, solution=None, kpi_filter=None, objective_key=None, use_names=True):
""" Returns KPI values in a solution as a dictionary.
Each KPI has a value in the solution. This method returns a dictionary of KPI values,
indexed by KPI objects.
:param solution: an instance of solution, as returned by solve(). If not passed, will
use the model's solution. If no solution is present, an exception is raised.
:param kpi_filter: an optional predicate to filter some kpis.
If provided, accepts a function taking one KPI as argument and
returning a boolean. By default, all KPIs are returned.
:param objective_key: an optional string key for th eobjective value. If present, the
value of the objective is added to the dictionary, with this key. By default, this parameter
is None and the objective is *not* appended to the dictionary.
:param use_names: a flag which determines whether keys in the resulting dict
are KPI objects or kpi names. Default is to use KPI names.
:return:
A dictionary mapping KPIs, or KPI names to values.
See Also:
:class:`docplex.mp.solution.SolveSolution`
"""
if kpi_filter is None:
kpi_filter = lambda _: True
if use_names:
kpi_dict = {kpi.name: kpi.compute(solution) for kpi in self.iter_kpis() if kpi_filter(kpi)}
else:
kpi_dict = {kpi: kpi.compute(solution) for kpi in self.iter_kpis() if kpi_filter(kpi)}
if objective_key:
kpi_dict[objective_key] = solution.objective_value
return kpi_dict
def _report_lexicographic_goals(self, goal_name_values, kpi_header_format): # pragma: no cover
kpi_format = kpi_header_format + self._float_meta_format % (1,) # be safe even integer KPIs might yield floats
printed_kpis = goal_name_values if is_iterable(goal_name_values) else self.iter_kpis()
for goal_name, goal_expr in printed_kpis:
goal_value = goal_expr.solution_value
print(kpi_format.format(goal_name, goal_value))
[docs] def iter_kpis(self):
""" Returns an iterator over all KPIs in the model.
Returns:
An iterator object.
"""
return iter(self._allkpis)
[docs] def kpi_by_name(self, name, try_match=True, match_case=False, do_raise=True):
""" Fetches a KPI from a string.
This method fetches a KPI from a string, using either exact naming or trying
to match a substring of the KPI name.
Args:
name (string): The string to be matched.
try_match (Boolean): If True, returns KPI whose name is not equal to the
argument, but contains it. Default is True.
match_case: If True, looks for a case-exact match, else ignores case. Default is False.
do_raise: If True, raise an exception when no KPI is found.
Example:
If the KPI name is "Total CO2 Cost" then fetching with argument `co2` and `match_case` to False
will succeed. If `match_case` is True, then no KPI will be returned.
Returns:
The KPI expression if found. If the search fails, either raises an exception or returns a dummy
constant expression with 0.
"""
matching_kpi = self._matching_kpi(name, try_match, match_case)
if matching_kpi:
return matching_kpi
# no match was found
if do_raise:
self.fatal("Model has no KPI with name matching: '{0:s}'", name)
else:
return self._lfactory.new_zero_expr()
def _matching_kpi(self, name, try_match=True, match_case=False):
# internal
for kpi in iter(reversed(self._allkpis)):
kpi_name = kpi.name
ok = False
if kpi_name == name:
ok = True
elif try_match:
if match_case:
ok = kpi_name.find(name) >= 0
else:
ok = kpi_name.lower().find(name.lower()) >= 0
if ok:
return kpi
return None
[docs] def kpi_value_by_name(self, name, solution=None, try_match=True, match_case=False):
""" Returns a KPI value from a KPI name.
This method fetches a KPI value from a string, using either exact naming or trying
to match a substring of the KPI name.
Args:
name (str): The string to be matched.
solution: an optional solution. If not present, assume the model is solved
and use the model solution.
try_match (Bool): If True, returns KPI whose name is not equal to the
argument, but contains it. Default is True.
match_case: If True, looks for a case-exact match, else ignores case. Default is False.
Example:
If the KPI name is "Total CO2 Cost" then fetching with argument `co2` and `match_case` to False
will succeed. If `match_case` is True, then no KPI will be returned.
Note:
KPIs require a solution to be evaluated. This solution can be passed explicitly as a parameter,
or the model is assumed to be solved with a valid solution.
Returns:
float: The KPI value.
See Also:
:class:`docplex.mp.solution.SolveSolution`
:func:`new_solution`
"""
kpi = self.kpi_by_name(name, try_match, match_case=match_case, do_raise=True)
return kpi.compute(solution)
[docs] def add_kpi(self, kpi_arg, publish_name=None):
""" Adds a Key Performance Indicator to the model.
Key Performance Indicators (KPIs) are objects that can be evaluated after a solve().
Typical use is with decision expressions, the evaluation of which return the expression's solution value.
KPI values are displayed with the method :func:`report_kpis`.
Args:
kpi_arg: Accepted arguments are either an expression,
a lambda function with two arguments (model + solution)
or an instance of a subclass of abstract class KPI.
publish_name (string, optional): The published name of the KPI.
Note:
- If no publish_name is provided, DOcplex will try to access a
'name' attribute of the argument; if none exists, it will use the
string representation of the argument , as returned by `str()`.
- expression KPIs are seperate from the model. In other terms,
adding KPIs does not change the model (and matrix) being solved.
Examples:
`model.add_kpi(x+y+z, "Total Profit")` adds the expression `(x+y+z)` as a KPI with the name "Total Profit".
`model.add_kpi(x+y+z)` adds the expression `(x+y+z)` as a KPI with
the name "x+y+z", assuming variables x,y,z have names 'x', 'y', 'z' (resp.)
Returns:
The newly added KPI instance.
See Also:
:class:`docplex.mp.kpi.KPI`,
:class:`docplex.mp.kpi.DecisionKPI`
"""
self._checker.typecheck_string(publish_name, accept_empty=False, accept_none=True, caller="Model.add_kpi(): ")
new_kpi = self._lfactory.new_kpi(kpi_arg, publish_name)
new_kpi_name = new_kpi.name
if new_kpi_name in set(kp.name for kp in self._allkpis):
self.fatal("Duplicate KPI name: \"{0!s}\" ", new_kpi_name)
self._allkpis.append(new_kpi)
return new_kpi
[docs] def remove_kpi(self, kpi_arg):
""" Removes a Key Performance Indicator from the model.
Args:
kpi_arg: A KPI instance that was previously added to the model. Accepts either a KPI object or a string.
If passed a string, looks for a KPI with that name.
See Also:
:func:`add_kpi`
:class:`docplex.mp.kpi.KPI`,
:class:`docplex.mp.kpi.DecisionKPI`
"""
if is_string(kpi_arg):
kpi = self.kpi_by_name(kpi_arg)
if kpi:
self._allkpis.remove(kpi)
kpi.notify_removed()
else:
for k, kp in enumerate(self._allkpis):
if kp is kpi_arg:
kx = k
break
else:
kx = -1
if kx >= 0:
removed_kpi = self._allkpis.pop(kx)
removed_kpi.notify_removed()
else:
self.warning('Model.remove_kpi(): cannot interpret this either as a string or as a KPI: {0!r} - ignored', kpi_arg)
[docs] def clear_kpis(self):
''' Clears all KPIs defined in the model.
'''
self._allkpis = []
@property
def number_of_kpis(self):
return len(self._allkpis)
[docs] def add_progress_listener(self, listener):
""" Adds a progress listener to the model.
A progress listener is a subclass of :class:~docplex.mp.ProgressListener:
:param listener:
"""
self._checker.typecheck_progress_listener(listener)
self._add_progress_listener(listener)
def _add_progress_listener(self, listener):
# INTERNAL
self._progress_listeners.append(listener)
def _add_qprogress_listener(self, qlistener):
self._qprogress_listeners.append(qlistener)
[docs] def remove_progress_listener(self, listener):
""" Remove a progress listener from the model.
:param listener:
"""
try:
self._progress_listeners.remove(listener)
except ValueError:
# ignore errors
if self.is_docplex_debug():
raise
else:
pass
[docs] def iter_progress_listeners(self):
""" Returns an iterator on the progress listeners attached to the model.
:return: an iterator.
"""
return iter(self._progress_listeners)
@property
def number_of_progress_listeners(self):
""" Returns the number of progress listeners attached to the model.
:return: an integer
"""
return len(self._progress_listeners)
[docs] def clear_progress_listeners(self):
""" Remove all progress listeners from the model."""
self._progress_listeners = []
def _clear_qprogress_listeners(self):
self._qprogress_listeners = []
def _fire_start_solve_listeners(self):
for l in self._progress_listeners:
l.notify_start()
def _fire_end_solve_listeners(self, has_solution, objective_value):
for l in self._progress_listeners:
l.notify_end(has_solution, objective_value)
def prettyprint(self, out=None):
from docplex.mp.ppretty import ModelPrettyPrinter
ppr = ModelPrettyPrinter()
ppr.printModel(self, out=out)
pprint = prettyprint
def pprint_as_string(self):
from docplex.mp.ppretty import ModelPrettyPrinter
with StringIO() as oss:
ppr = ModelPrettyPrinter()
ppr.printModel(self, out=oss)
return oss.getvalue()
[docs] def clone(self, new_name=None, **clone_kwargs):
""" Makes a deep copy of the model, possibly with a new name.
Decision variables, constraints, and objective are copied.
Args:
new_name (string): The new name to use. If None is provided, returns a "Copy of xxx" where xxx is the original model name.
:returns: A new model.
:rtype: :class:`docplex.mp.model.Model`
"""
return self.copy(new_name=new_name, **clone_kwargs)
def copy(self, new_name=None, removed_cts=None, **new_kwargs):
# INTERNAL
actual_copy_name = new_name or "Copy of %s" % self.name
# copy kwargs
copy_kwargs = self._get_kwargs().copy()
copy_kwargs.update(**new_kwargs)
copy_context = self.context.copy()
# pass copy of initial context
# plus override kwargs (e.g. log_output)
copy_model = Model(name=actual_copy_name, context=copy_context, **copy_kwargs)
# clone variable containers
ctn_map = {}
for ctn in self.iter_var_containers():
copied_ctn = ctn.copy(copy_model)
ctn_map[ctn] = copied_ctn
copy_model._add_var_container(copied_ctn)
# clone variables
def make_memo():
memo = {}
generated_vars = []
# clone PWL functions and add them to var_mapping
for pwl_func in self.iter_pwl_functions():
copied_pwl_func = pwl_func.copy(copy_model, memo)
memo[pwl_func] = copied_pwl_func
# copy 'primary' variables
for v in self.iter_variables():
if v.is_generated():
generated_vars.append(v)
else:
copied_var = copy_model._var(v.vartype, v.lb, v.ub, v.name)
var_ctn = v.container
if var_ctn:
copied_ctn = ctn_map.get(var_ctn)
assert copied_ctn is not None
copied_var.container = copied_ctn
memo[v] = copied_var
for gv in generated_vars:
gvoo = gv.origin
try:
gvo, gvx = gvoo
except TypeError:
gvo = gvoo
gvx = 0
assert gvo is not None
gvk = id(gvo)
cloned_origin = memo.get(gvk)
if cloned_origin is None:
cloned_origin = gvo.copy(copy_model, memo)
cloned_origin.resolve()
memo[gvk] = cloned_origin
cloned_gv = cloned_origin.get_artefact(gvx)
assert cloned_gv
memo[gv] = cloned_gv
return memo
memo = make_memo()
# copy constraints
setof_removed_cts = set(removed_cts) if removed_cts else {}
linear_cts = []
for ct in self.iter_constraints():
if not ct.is_generated() and ct not in setof_removed_cts:
if isinstance(ct, PwlConstraint):
continue
if ct.is_linear():
linear_cts.append(ct.copy(copy_model, memo))
elif ct.is_logical:
if linear_cts:
# add stored linear cts
copy_model.add_constraints(linear_cts)
linear_cts = []
copy_model.add(ct.copy(copy_model, memo))
if linear_cts:
copy_model.add_constraints(linear_cts)
# clone objective
copy_model.set_objective_sense(self.objective_sense)
if self.has_multi_objective():
multi_objective = self._multi_objective
exprs = multi_objective.exprs
nb_exprs = len(exprs)
copied_exprs = [expr.copy(copy_model, memo) for expr in exprs]
copy_model.set_multi_objective(self.objective_sense,
exprs=copied_exprs,
priorities=multi_objective.priorities,
weights=multi_objective.weights,
abstols=MultiObjective.as_optional_sequence(multi_objective.abstols,
nb_exprs),
reltols=MultiObjective.as_optional_sequence(multi_objective.reltols,
nb_exprs),
names=multi_objective.names)
else:
copy_model.set_objective(self.objective_sense, self.objective_expr.copy(copy_model, memo))
# clone kpis
for kpi in self.iter_kpis():
copy_model.add_kpi(kpi.copy(copy_model, memo))
# clone sos
for sos in self.iter_sos():
if not sos.is_generated():
copy_model._create_engine_sos(sos.copy(copy_model, memo))
# parameters
for p in self.parameters.iter_params():
if p.is_nondefault():
# copy value to new parames
newp = copy_model.get_parameter_from_id (p.cpx_id)
if newp:
newp.set(p.value)
return copy_model
[docs] def end(self):
""" Terminates a model instance.
Since this method destroys the objects associated with the model, you must not use the model
after you call this member function.
This method must be called when you don't need a CPLEX engine anymore to free resources,
unless the docplex.Model was created with the `with` keyword.
"""
self._clear_internal(terminate=True)
@property
def parameters(self):
""" This property returns the root parameter group of the model.
The root parameter group models the parameter hierarchy.
It is the way to access any CPLEX parameter and get or set its value.
Examples:
.. code-block:: python
model.parameters.mip.tolerances.mipgap
Returns the parameter itself, an instance of the `Parameter` class.
To get the value of the parameter, use the `get()` method, as in:
.. code-block:: python
model.parameters.mip.tolerances.mipgap.get()
>>> 0.0001
To change the value of the parameter, use a standard Python assignment:
.. code-block:: python
model.parameters.mip.tolerances.mipgap = 0.05
model.parameters.mip.tolerances.mipgap.get()
>>> 0.05
Assignment is equivalent to the `set()` method:
.. code-block:: python
model.parameters.mip.tolerances.mipgap.set(0.02)
model.parameters.mip.tolerances.mipgap.get()
>>> 0.02
Returns:
The root parameter group, an instance of the `ParameterGroup` class.
"""
context_params = self.context.cplex_parameters
if not self._synced_params:
self._sync_params(context_params)
self._synced_params = True
return context_params
[docs] def get_parameter_from_id(self, parameter_cpx_id):
""" Finds a parameter from a CPLEX id code.
Args:
parameter_cpx_id: A CPLEX parameter id (positive integer, for example, 2009 is mipgap).
:returns: An instance of :class:`docplex.mp.params.parameters.Parameter` if found, else None.
"""
assert parameter_cpx_id >= 0
for p in self.parameters.generate_params():
if p.cpx_id == parameter_cpx_id:
return p
return None
def get_engine_parameter_value(self, param):
return self.__engine.get_parameter(param)
def apply_parameters(self):
self._apply_parameters_to_engine(self.parameters)
def apply_one_parameter(self, param):
# internal
self.__engine.set_parameter(param, param.value)
def _apply_parameters_to_engine(self, parameters_to_use):
# internal
if parameters_to_use is not None:
self_engine = self.__engine
for param in parameters_to_use:
self_engine.set_parameter(param, param.value)
def _get_cplex_engine(self, caller):
# INTERNAL
self_engine = self.get_engine()
if self_engine.name != 'cplex_local':
self.fatal("{1} is only for Cplex, engine is '{0}'", self_engine.name, str(caller))
else:
return self_engine
def set_hidden_parameter(self, parameter_id, param_value):
self_engine = self._get_cplex_engine(caller="Model.set_hidden_parameter")
self_engine.set_parameter_from_id(parameter_id, param_value)
def get_hidden_parameter(self, parameter_id):
self_engine = self._get_cplex_engine(caller="Model.get_hidden_parameter")
return self_engine.get_parameter_from_id(parameter_id)
# with protocol
def __enter__(self):
return self
def __exit__(self, atype, avalue, atraceback):
# terminate the model upon exiting a 'with' block.
self.end()
def __iadd__(self, e):
# implements the "+=" dialect a la PulP
self.add(e)
return self
def _resync(self):
# INTERNAL
self._lfactory.resync_whole_model()
def resync_engine(self):
# INTERNAL: resync after pickle
self.__engine.resync()
def sync_cplex_engine(self):
if self.has_cplex():
eng = self.get_engine()
eng.sync_cplex()
else:
self.warning('Model has no cplex, sync operation ignored.')
[docs] def add_sos1(self, dvars, name=None):
''' Adds an SOS of type 1 to the model.
Args:
dvars: The variables in the special ordered set.
This method only accepts ordered sequences of variables or iterators.
Unordered iterables (e.g. dictionaries or sets) are not accepted.
name: An optional name.
Returns:
The newly added SOS.
'''
return self.add_sos(dvars, sos_arg=SOSType.SOS1, name=name)
[docs] def add_sos2(self, dvars, name=None):
''' Adds an SOS of type 2 to the model.
Args:
dvars: The variables in the specially ordered set.
This method only accepts ordered sequences of variables or iterators.
Unordered iterables (e.g. dictionaries or sets) are not accepted.
name: An optional name.
Returns:
The newly added SOS.
'''
return self.add_sos(dvars, sos_arg=SOSType.SOS2, name=name)
[docs] def add_sos(self, dvars, sos_arg, weights=None, name=None):
''' Adds an SOS to the model.
Args:
sos_arg: The SOS type. Valid values are numerical (1 and 2) or enumerated (`SOSType.SOS1` and
`SOSType.SOS2`).
dvars: The variables in the special ordered set.
This method only accepts ordered sequences of variables or iterators,
e.g. lists, numpy arrays, pandas Series.
Unordered iterables (e.g. dictionaries or sets) are not accepted.
weights: optional weights. Accepts None (no weights) or a list of numbers, with the same size as
number of variables.
name: An optional name.
Returns:
The newly added SOS.
'''
sos_type = SOSType.parse(sos_arg)
msg = 'Model.add_%s() expects an ordered sequence (or iterator) of variables' % sos_type.lower()
self._checker.check_ordered_sequence(arg=dvars, caller=msg)
var_seq = self._checker.typecheck_var_seq(dvars, caller="Model.add_sos")
var_list = list(var_seq) # we need len here.
nb_vars = len(var_list)
if nb_vars < sos_type.size:
self.fatal("A {0:s} variable set must contain at least {1:d} variables, got: {2:d}",
sos_type.name, sos_type.size, nb_vars)
elif nb_vars == sos_type.size:
self.warning("{0:s} variable is trivial, contains {1} variable(s): all variables set to 1",
sos_type.name, sos_type.size)
lweights = StaticTypeChecker.typecheck_optional_num_seq(self, weights, accept_none=True, expected_size=nb_vars,
caller='Model.add_sos')
return self._add_sos(dvars, sos_type, weights=lweights, name=name)
def _add_sos(self, dvars, sos_type, weights=None, name=None):
# INTERNAL
new_sos = self._lfactory.new_sos(dvars, sos_type=sos_type, weights=weights, name=name)
sos_index = self.__engine.create_sos(new_sos)
self._register_sos(new_sos, sos_index)
new_sos._set_index(sos_index)
return new_sos
def _create_engine_sos(self, new_sos):
# internal
sos_index = self.__engine.create_sos(new_sos)
self._register_sos(new_sos, sos_index)
new_sos._set_index(sos_index)
def _register_sos(self, new_sos, sos_index):
self._sos_scope.notify_obj_index(new_sos, sos_index)
def _get_sos_by_index(self, sos_idx):
return self._sos_scope.get_object_by_index(sos_idx)
[docs] def iter_sos(self):
''' Iterates over all SOS sets in the model.
Returns:
An iterator object.
'''
return self._sos_scope.iter_objects()
@property
def number_of_sos(self):
''' This property returns the total number of SOS sets in the model.
'''
return self._sos_scope.size
[docs] def clear_sos(self):
''' Clears all SOS sets in the model.
'''
self._sos_scope.clear()
self.__engine.clear_all_sos()
def _generate_sos(self, sos_type):
# INTERNAL
for sos_set in self.iter_sos():
if sos_set.sos_type == sos_type:
yield sos_set
[docs] def iter_sos1(self):
''' Iterates over all SOS1 sets in the model.
Returns:
An iterator object.
'''
return self._generate_sos(SOSType.SOS1)
[docs] def iter_sos2(self):
''' Iterates over all SOS2 sets in the model.
Returns:
An iterator object.
'''
return self._generate_sos(SOSType.SOS2)
@property
def number_of_sos1(self):
''' This property returns the total number of SOS1 sets in the model.
'''
return sum(1 for _ in self.iter_sos1())
@property
def number_of_sos2(self):
''' This property returns the total number of SOS2 sets in the model.
'''
return sum(1 for _ in self.iter_sos2())
[docs] def piecewise(self, preslope, breaksxy, postslope, name=None):
""" Adds a piecewise linear function (PWL) to the model, using breakpoints to specify the function.
Args:
preslope: Before the first segment of the PWL function there is a half-line; its slope is specified by
this argument.
breaksxy: A list `(x[i], y[i])` of coordinate pairs defining segments of the PWL function.
postslope: After the last segment of the the PWL function there is a half-line; its slope is specified by
this argument.
name: An optional name.
Example::
# Creates a piecewise linear function whose value if '0' if the `x_value` is `0`, with a slope
# of -1 for negative values and +1 for positive value
model = Model('my model')
model.piecewise(-1, [(0, 0)], 1)
# Note that a PWL function may be discontinuous. Here is an example of a step function:
model.piecewise(0, [(0, 0), (0, 1)], 0)
Returns:
The newly added piecewise linear function.
"""
if breaksxy is None:
self._checker.fatal("argument 'breaksxy' must be defined")
StaticTypeChecker.typecheck_num_nan_inf(self, preslope, caller='Model.piecewise.preslope')
StaticTypeChecker.typecheck_num_nan_inf(self, postslope, caller='Model.piecewise.postslope')
PwlFunction.check_list_pair_breaksxy(self._checker, breaksxy)
return self._piecewise(PwlFunction._PwlAsBreaks(preslope, breaksxy, postslope), name)
[docs] def piecewise_as_slopes(self, slopebreaksx, lastslope, anchor=(0, 0), name=None):
""" Adds a piecewise linear function (PWL) to the model, using a list of slopes and x-coordinates.
Args:
slopebreaksx: A list of tuple pairs `(slope[i], breakx[i])` of slopes and x-coordinates defining the slope of
the piecewise function between the previous breakpoint (or minus infinity if there is none)
and the breakpoint with x-coordinate `breakx[i]`.
For representing a discontinuity, two consecutive pairs with the same value for `breakx[i]`
are used. The value of `slope[i]` in the second pair is the discontinuity gap.
lastslope: The slope after the last specified breakpoint.
anchor: The coordinates of the 'anchor point'. The purpose of the anchor point is to ground the piecewise
linear function specified by the list of slopes and breakpoints.
name: An optional name.
Example::
# Creates a piecewise linear function whose value if '0' if the `x_value` is `0`, with a slope
# of -1 for negative values and +1 for positive value
model = Model('my model')
model.piecewise_as_slopes([(-1, 0)], 1, (0, 0))
# Here is the definition of a step function to illustrate the case of a discontinuous PWL function:
model.piecewise_as_slopes([(0, 0), (0, 1)], 0, (0, 0))
Returns:
The newly added piecewise linear function.
"""
StaticTypeChecker.typecheck_num_nan_inf(self, lastslope, caller="Model.piecewise_as_slopes.lastslope")
StaticTypeChecker.check_number_pair(self, anchor, caller="Model.piecewise_as_slopes.anchor")
PwlFunction.check_list_pair_slope_breakx(self, slopebreaksx, anchor)
return self._piecewise(PwlFunction._PwlAsSlopes(slopebreaksx, lastslope, anchor), name)
def add_piecewise_constraint(self, y, pwlf, x, name=None):
checker = self._checker
checker.typecheck_continuous_var(x)
checker.typecheck_continuous_var(y)
checker.typecheck_pwl_function(pwlf)
if x is y:
self.fatal('Piecewise-linear constraint requires two different variables, only one wa passed: {0}', x)
pwl_expr = self._add_pwl_expr(pwlf, arg=x, yvar=y, resolve=False) # will be resolved later
pwl_ct = self._lfactory.new_pwl_constraint(pwl_expr, name)
return self._add_pwl_constraint_internal(pwl_ct)
def _piecewise(self, pwl_def, name=None):
pwl_func = self._lfactory.new_piecewise(pwl_def, name)
self.__allpwlfuncs.append(pwl_func)
return pwl_func
def _add_pwl_expr(self, pwl_func, arg, yvar=None, resolve=True):
pwl_expr = self._lfactory.new_pwl_expr(pwl_func, arg, y_var=yvar, resolve=resolve)
return pwl_expr
def _add_pwl_constraint_internal(self, pwlct):
ct_engine_index = self.__engine.create_pwl_constraint(pwlct)
self._register_one_pwl_constraint(pwlct, ct_engine_index)
return pwlct
def _register_one_pwl_constraint(self, new_pwl_ct, ct_index):
self.__notify_new_model_object(
"pwl", new_pwl_ct, ct_index, mobj_name=None, name_dir=None, idx_scope=self._pwl_scope, is_name_safe=True)
[docs] def iter_pwl_constraints(self):
""" Iterates over all PWL constraints in the model.
Returns:
An iterator object.
"""
return self._pwl_scope.iter_objects()
@property
def number_of_pwl_constraints(self):
""" This property returns the total number of PWL constraints in the model.
"""
return self._pwl_scope.size
def _ensure_benders_annotations(self):
if self._benders_annotations is None:
self._benders_annotations = {}
return self._benders_annotations
def set_benders_annotation(self, obj, group):
if group is None:
self_benders = self._benders_annotations
if self_benders is not None and obj in self_benders:
del self_benders[obj]
else:
self._checker.typecheck_int(group, accept_negative=False, caller='Model.set_benders_annotation')
self._ensure_benders_annotations()[obj] = group
def remove_benders_annotation(self, obj):
self_benders = self._benders_annotations
if self_benders:
del self_benders[obj]
def get_benders_annotation(self, obj):
self_benders = self._benders_annotations
return self_benders.get(obj) if self_benders is not None else None
def iter_benders_annotations(self):
self_benders = self._benders_annotations
return self_benders.items() if self_benders is not None else iter([])
def clear_benders_annotations(self):
self._benders_annotations = None
def get_annotations_by_scope(self):
# INTERNAL
from collections import defaultdict
annotated_by_scope = defaultdict(list)
for obj, group in self.iter_benders_annotations():
annotated_by_scope[obj.cplex_scope].append((obj, group))
return annotated_by_scope
def has_benders_annotations(self):
self_benders = self._benders_annotations
return bool(self_benders)
def get_annotation_stats(self):
from collections import Counter
annotated_by_scope = Counter()
for obj, group in self.iter_benders_annotations():
annotated_by_scope[obj.cplex_scope] += 1
return annotated_by_scope
def register_callback(self, cb_type):
# Registers a callback with the model.
#
# Assumes the type has a `model` setter property. Use a subclass of `ModelCallbackMixin` mixin class
# as a parent class to ensure this.
#
# :param cb_type: a callback type; the type must be a subtype of some cplex callback type
#
# :return: an instance of the callback
#
cplex_cb = self.__engine.register_callback(cb_type)
if cplex_cb:
cplex_cb._model = self
return cplex_cb
def _resolve_pwls(self):
# INTERNAL
self._objective_expr.resolve()
no_pwl_scopes = [self._linct_scope, self._logical_scope, self._quadct_scope]
for sc in no_pwl_scopes:
for x in sc.iter_objects():
x.resolve()
# this call updates the dict so we must iterate on something else.
#pwls = [pw for pw in self._pwl_scope.iter_objects()]
pwls = list(self._pwl_scope.iter_objects())
# BEWARE: need this temp list as resolve will modify the dict, so canno titerate on it.
for pw in pwls:
pw.resolve()
def get_constraint_priority(self, ct):
# INTERNAL
return self._constraint_priority_dict.get(ct) # return None if not found
def set_constraint_priority(self, ct, prio):
# INTERNAL
self._constraint_priority_dict[ct] = prio
def _extend_constraint_section(self, collector, extra_cts, ctnames, caller):
# INTERNAL
checker = self._checker
new_cts = checker.typecheck_constraint_seq(extra_cts, check_linear=True, accept_range=False)
if ctnames is not None:
checker.typecheck_iterable(ctnames)
for ct, ctn in izip2_filled(new_cts, ctnames):
checker.typecheck_string(ctn, accept_none=True)
if ctn:
self._register_ct_name(ct, ctn, checker)
# extend
lncts = list(new_cts)
for nc in lncts:
checker.typecheck_ct_not_added(nc, do_raise=False, caller=caller)
collector.extend(new_cts)
return new_cts
[docs] def add_lazy_constraints(self, lazy_cts, names=None):
"""Adds lazy constraints to the problem.
This method expects an iterable returning linear constraints (ranges are not accepted).
:param lazy_cts: an iterable returning linear constraints (not ranges)
:param names: an optional iterable returning strings, used to set names for lazy constraints.
*New in version 2.10*
"""
new_lazy_cts = self._extend_constraint_section(self._lazy_constraints, lazy_cts, names, caller='Model.add_lazy_constraints')
for nc in new_lazy_cts:
nc.notify_used_as_lazy_constraint()
self.__engine.add_lazy_constraints(new_lazy_cts)
[docs] def add_lazy_constraint(self, lazy_ct, name=None):
"""Adds one lazy constraint to the problem.
This method expects a linear constraint.
:param lazy_ct: a linear constraints (ranges are not accepted)
:param name: an optional string, used to set the name of the lazy constraint.
*New in version 2.10*
"""
self.add_lazy_constraints((lazy_ct,), names=(name,))
[docs] def clear_lazy_constraints(self):
"""
Clears all lazy constraints from the model.
*New in version 2.10*
"""
old_lazy_cts = self._lazy_constraints
for lz in old_lazy_cts:
lz.notify_unused_as_lazy_constraint()
self._lazy_constraints = []
self.__engine.clear_lazy_constraints()
[docs] def iter_lazy_constraints(self):
""" Returns an iterator on the model's lazy constraints
:return: an iterator on lazy constraints.
*New in version 2.10*
"""
return iter(self._lazy_constraints)
@property
def number_of_lazy_constraints(self):
"""Returns the number of lazy constraints present in the model
"""
return len(self._lazy_constraints)
def _is_lazy_constraint(self, lineart_ct):
# INTERNAL
return any(lc is lineart_ct for lc in self.iter_lazy_constraints())
[docs] def add_user_cut_constraints(self, cut_cts, names=None):
"""Adds user cut constraints to the problem.
This method expects an iterable returning linear constraints (ranges are not accepted).
:param cut_cts: an iterable returning linear constraints (not ranges)
:param names: an optional iterable returning strings, used to set names for user cut constraints.
*New in version 2.10*
"""
new_user_cuts = self._extend_constraint_section(self._user_cuts, cut_cts, names, caller='Model.add_user_cut_constraints')
for nc in new_user_cuts:
nc.notify_used_as_user_cut()
self.__engine.add_user_cuts(new_user_cuts)
[docs] def add_user_cut_constraint(self, cut_ct, name=None):
"""Adds one user cut constraint to the problem.
This method expects a linear constraint.
:param cut_ct: a linear constraints (ranges are not accepted)
:param name: an optional string, used to set the name for the cut constraint.
*New in version 2.10*
"""
self.add_user_cut_constraints((cut_ct,), names=(name,))
[docs] def clear_user_cut_constraints(self):
"""
Clears all user cut constraints from the model.
*New in version 2.10*
"""
old_user_cuts = self._user_cuts
for uc in old_user_cuts:
uc.notify_unused_as_user_cut()
self._user_cuts = []
self.__engine.clear_user_cuts()
[docs] def iter_user_cut_constraints(self):
""" Returns an iterator on the model's user cut constraints
:return: an iterator on user cut constraints.
*New in version 2.10*
"""
return iter(self._user_cuts)
@property
def number_of_user_cut_constraints(self):
"""Returns the number of user cut constraints present in the model
*New in version 2.10*
"""
return len(self._user_cuts)
def _is_user_cut_constraint(self, lineart_ct):
# INTERNAL
return any(lc is lineart_ct for lc in self.iter_user_cut_constraints())