class Model: """ Mixed Integer Programming Model This is the main class, providing methods for building, optimizing, querying optimization results and re-optimizing Mixed-Integer Programming Models. To check how models are created please see the examples included. """ def __init__(self, name: str = "", sense: str = MINIMIZE, solver_name: str = ""): """Model constructor Creates a Mixed-Integer Linear Programming Model. The default model optimization direction is Minimization. To store and optimize the model the MIP package automatically searches and connects in runtime to the dynamic library of some MIP solver installed on your computer, nowadays gurobi and cbc are supported. This solver is automatically selected, but you can force the selection of a specific solver with the parameter solver_name. Args: name (str): model name sense (str): MINIMIZATION ("MIN") or MAXIMIZATION ("MAX") solver_name: gurobi or cbc, searches for which solver is available if not informed """ # initializing variables with default values self.name = name self.solver_name = solver_name self.solver = None # reading solver_name from an environment variable (if applicable) if not self.solver_name and "solver_name" in environ: self.solver_name = environ["solver_name"] if not self.solver_name and "solver_name".upper() in environ: self.solver_name = environ["solver_name".upper()] # creating a solver instance if self.solver_name.upper() == GUROBI: from mip.gurobi import SolverGurobi self.solver = SolverGurobi(self, self.name, sense) elif self.solver_name.upper() == CBC: from mip.cbc import SolverCbc self.solver = SolverCbc(self, self.name, sense) else: # checking which solvers are available from mip import gurobi if gurobi.has_gurobi: from mip.gurobi import SolverGurobi self.solver = SolverGurobi(self, self.name, sense) self.solver_name = GUROBI else: from mip.cbc import SolverCbc self.solver = SolverCbc(self, self.name, sense) self.solver_name = CBC # list of constraints and variables self.constrs = ConstrList(self) self.vars = VarList(self) # initializing additional control variables self.__cuts = 1 self.__cuts_generator = None self.__lazy_constrs_generator = None self.__start = None self.__status = OptimizationStatus.LOADED self.__threads = 0 self.__n_cols = 0 self.__n_rows = 0 def __del__(self): if self.solver: del self.solver def __iadd__(self, other) -> "Model": if isinstance(other, LinExpr): if len(other.sense) == 0: # adding objective function components self.objective = other else: # adding constraint self.add_constr(other) elif isinstance(other, tuple): if isinstance(other[0], LinExpr) and isinstance(other[1], str): if len(other[0].sense) == 0: self.objective = other[0] else: self.add_constr(other[0], other[1]) return self def add_var(self, name: str = "", lb: float = 0.0, ub: float = INF, obj: float = 0.0, var_type: str = CONTINUOUS, column: "Column" = None) -> "Var": """ Creates a new variable in the model, returning its reference Args: name (str): variable name (optional) lb (float): variable lower bound, default 0.0 ub (float): variable upper bound, default infinity obj (float): coefficient of this variable in the objective function, default 0 var_type (str): CONTINUOUS ("C"), BINARY ("B") or INTEGER ("I") column (Column): constraints where this variable will appear, necessary only when constraints are already created in the model and a new variable will be created. Examples: To add a variable :code:`x` which is continuous and greater or equal to zero to model :code:`m`:: x = m.add_var() The following code creates a vector of binary variables :code:`x[0], ..., x[n-1]` to model :code:`m`:: x = [m.add_var(var_type=BINARY) for i in range(n)] """ if var_type == BINARY: lb = 0.0 ub = 1.0 if len(name.strip()) == 0: nc = self.solver.num_cols() name = "C{:011d}".format(nc) self.solver.add_var(obj, lb, ub, var_type, column, name) self.__n_cols += 1 return Var(self, self.__n_cols - 1) def add_constr(self, lin_expr: "LinExpr", name: str = "") -> "Constr": """ Creates a new constraint (row) Adds a new constraint to the model, returning its reference Args: lin_expr (LinExpr): linear expression name (str): optional constraint name, used when saving model to\ lp or mps files Examples: The following code adds the constraint :math:`x_1 + x_2 \leq 1` (x1 and x2 should be created first using :func:`add_var<mip.model.Model.add_var>`):: m += x1 + x2 <= 1 Which is equivalent to:: m.add_constr( x1 + x2 <= 1 ) Summation expressions can be used also, to add the constraint \ :math:`\displaystyle \sum_{i=0}^{n-1} x_i = y` and name this \ constraint :code:`cons1`:: m += xsum(x[i] for i in range(n)) == y, "cons1" Which is equivalent to:: m.add_constr( xsum(x[i] for i in range(n)) == y, "cons1" ) """ if isinstance(lin_expr, bool): raise InvalidLinExpr("A boolean (true/false) cannot be \ used as a constraint.") self.__n_rows += 1 self.solver.add_constr(lin_expr, name) return Constr(self, self.__n_rows - 1) def clear(self): """Clears the model All variables, constraints and parameters will be reset. In addition, a new solver instance will be instantiated to implement the formulation. """ # creating a new solver instance sense = self.sense self.__n_cols = 0 self.__n_rows = 0 if self.solver_name.upper() == GUROBI: from mip.gurobi import SolverGurobi self.solver = SolverGurobi(self, self.name, sense) elif self.solver_name.upper() == CBC: from mip.cbc import SolverCbc self.solver = SolverCbc(self, self.name, sense) else: # checking which solvers are available from mip import gurobi if gurobi.has_gurobi: from mip.gurobi import SolverGurobi self.solver = SolverGurobi(self, self.name, sense) self.solver_name = GUROBI else: from mip.cbc import SolverCbc self.solver = SolverCbc(self, self.name, sense) self.solver_name = CBC # list of constraints and variables self.constrs = ConstrList(self) self.vars = VarList(self) # initializing additional control variables self.__cuts = 1 self.__cuts_generator = None self.__start = [] self.__status = OptimizationStatus.LOADED self.__threads = 0 def copy(self, solver_name: str = None) -> "Model": """ Creates a copy of the current model Args: solver_name(str): solver name (optional) Returns: clone of current model """ if not solver_name: solver_name = self.solver_name copy = Model(self.name, self.sense, solver_name) # adding variables for v in self.vars: copy.add_var(name=v.name, lb=v.lb, ub=v.ub, obj=v.obj, var_type=v.var_type) # adding constraints for c in self.constrs: orig_expr = c.expr expr = LinExpr(const=orig_expr.const, sense=orig_expr.sense) for (var, value) in orig_expr.expr.items(): expr.add_term(self.vars[var.idx], value) copy.add_constr(lin_expr=expr, name=c.name) # setting objective function"s constant copy.objective_const = self.objective_const return copy def constr_by_name(self, name: str) -> "Constr": """ Queries a constraint by its name Args: name(str): constraint name Returns: constraint or None if not found """ cidx = self.solver.constr_get_index(name) if cidx < 0 or cidx > len(self.constrs): return None return self.constrs[cidx] def var_by_name(self, name: str) -> "Var": """Searchers a variable by its name Returns: Variable or None if not found """ v = self.solver.var_get_index(name) if v < 0 or v > len(self.vars): return None return self.vars[v] def optimize(self, max_seconds: float = inf, max_nodes: int = inf, max_solutions: int = inf) -> OptimizationStatus: """ Optimizes current model Optimizes current model, optionally specifying processing limits. To optimize model :code:`m` within a processing time limit of 300 seconds:: m.optimize(max_seconds=300) Args: max_seconds (float): Maximum runtime in seconds (default: inf) max_nodes (float): Maximum number of nodes (default: inf) max_solutions (float): Maximum number of solutions (default: inf) Returns: optimization status, which can be OPTIMAL(0), ERROR(-1), INFEASIBLE(1), UNBOUNDED(2). When optimizing problems with integer variables some additional cases may happen, FEASIBLE(3) for the case when a feasible solution was found but optimality was not proved, INT_INFEASIBLE(4) for the case when the lp relaxation is feasible but no feasible integer solution exists and NO_SOLUTION_FOUND(5) for the case when an integer solution was not found in the optimization. """ if self.__threads != 0: self.solver.set_num_threads(self.__threads) # self.solver.set_callbacks(branch_selector, # incumbent_updater, lazy_constrs_generator) self.solver.set_processing_limits(max_seconds, max_nodes, max_solutions) self.__status = self.solver.optimize() return self.__status def read(self, path: str): """Reads a MIP model in :code:`.lp` or :code:`.mps` format. Note: all variables, constraints and parameters from the current model will be cleared. Args: path(str): file name """ self.clear() self.solver.read(path) self.__n_cols = self.solver.num_cols() self.__n_rows = self.solver.num_rows() def relax(self): """ Relax integrality constraints of variables Changes the type of all integer and binary variables to continuous. Bounds are preserved. """ self.solver.relax() for v in self.vars: if v.type == BINARY or v.type == INTEGER: v.type = CONTINUOUS def write(self, path: str): """Saves the MIP model, using the extension :code:`.lp` or :code:`.mps` to specify the file format. Args: path(str): file name """ self.solver.write(path) @property def objective_bound(self) -> float: return self.solver.get_objective_bound() @property def objective(self) -> LinExpr: """The objective function of the problem as a linear expression. Examples: The following code adds all :code:`x` variables :code:`x[0], ..., x[n-1]`, to the objective function of model :code:`m` with the same cost :code:`w`:: m.objective = xsum(w*x[i] for i in range(n)) A simpler way to define the objective function is the use of the model operator += :: m += xsum(w*x[i] for i in range(n)) Note that the only difference of adding a constraint is the lack of a sense and a rhs. """ return self.solver.get_objective() @objective.setter def objective(self, objective): if isinstance(objective, int) or isinstance(objective, float): self.solver.set_objective(LinExpr([], [], objective)) elif isinstance(objective, Var): self.solver.set_objective(LinExpr([objective], [1])) elif isinstance(objective, LinExpr): self.solver.set_objective(objective) @property def verbose(self) -> int: """0 to disable solver messages printed on the screen, 1 to enable """ return self.solver.get_verbose() @verbose.setter def verbose(self, verbose: int): self.solver.set_verbose(verbose) @property def threads(self) -> int: """number of threads to be used when solving the problem. 0 uses solver default configuration, -1 uses the number of available processing cores and :math:`\geq 1` uses the specified number of threads. An increased number of threads may improve the solution time but also increases the memory consumption.""" return self.__threads @threads.setter def threads(self, threads: int): self.__threads = threads @property def sense(self) -> str: """ The optimization sense Returns: the objective function sense, MINIMIZE (default) or (MAXIMIZE) """ return self.solver.get_objective_sense() @sense.setter def sense(self, sense: str): self.solver.set_objective_sense(sense) @property def objective_const(self) -> float: """Returns the constant part of the objective function """ return self.solver.get_objective_const() @objective_const.setter def objective_const(self, objective_const: float): self.solver.set_objective_const(objective_const) @property def objective_value(self) -> float: """Objective function value of the solution found """ return self.solver.get_objective_value() @property def num_solutions(self) -> int: """Number of solutions found during the MIP search Returns: number of solutions stored in the solution pool """ return self.solver.get_num_solutions() @property def objective_values(self) -> List[float]: """List of costs of all solutions in the solution pool Returns: costs of all solutions stored in the solution pool as an array from 0 (the best solution) to :attr:`~mip.model.model.num_solutions`-1. """ return [ float(self.solver.get_objective_value_i(i)) for i in range(self.num_solutions) ] @property def cuts_generator(self) -> "CutsGenerator": """Cut generator callback. Cut generators are called whenever a solution where one or more integer variables appear with continuous values. A cut generator will try to produce one or more inequalities to remove this fractional point. """ return self.__cuts_generator @cuts_generator.setter def cuts_generator(self, cuts_generator: "CutsGenerator"): self.__cuts_generator = cuts_generator @property def lazy_constrs_generator(self) -> "LazyConstrsGenerator": return self.__lazy_constrs_generator @lazy_constrs_generator.setter def lazy_constrs_generator(self, lazy_constrs_generator: "LazyConstrsGenerator"): self.__lazy_constrs_generator = lazy_constrs_generator @property def emphasis(self) -> SearchEmphasis: """defines the main objective of the search, if set to 1 (FEASIBILITY) then the search process will focus on try to find quickly feasible solutions and improving them; if set to 2 (OPTIMALITY) then the search process will try to find a provable optimal solution, procedures to further improve the lower bounds will be activated in this setting, this may increase the time to produce the first feasible solutions but will probably pay off in longer runs; the default option if 0, where a balance between optimality and feasibility is sought. """ return self.solver.get_emphasis() @emphasis.setter def emphasis(self, emphasis: SearchEmphasis): self.solver.set_emphasis(emphasis) @property def cuts(self) -> int: """controls the generation of cutting planes, 0 disables completely, 1 (default) generates cutting planes in a moderate way, 2 generates cutting planes aggressively and 3 generates even more cutting planes. Cutting planes usually improve the LP relaxation bound but also make the solution time of the LP relaxation larger, so the overall effect is hard to predict and experimenting different values for this parameter may be beneficial. """ return self.__cuts @cuts.setter def cuts(self, cuts: int): if cuts < 0 or cuts > 3: print('Warning: invalid value ({}) for parameter cuts, \ keeping old setting.'.format(self.__cuts)) self.__cuts = cuts @property def start(self) -> List[Tuple["Var", float]]: """Initial feasible solution Enters an initial feasible solution. Only the main binary/integer decision variables which appear with non-zero values in the initial feasible solution need to be informed. Auxiliary or continuous variables are automatically computed. """ return self.__start @start.setter def start(self, start: List[Tuple["Var", float]]): self.__start = start self.solver.set_start(start) @property def num_cols(self) -> int: """number of columns (variables) in the model""" return self.__n_cols @property def num_int(self) -> int: """number of integer variables in the model""" return self.solver.num_int() @property def num_rows(self) -> int: """number of rows (constraints) in the model""" return self.__n_rows @property def num_nz(self) -> int: """number of non-zeros in the constraint matrix""" return self.solver.num_nz() @property def cutoff(self) -> float: """upper limit for the solution cost, solutions with cost > cutoff will be removed from the search space, a small cutoff value may significantly speedup the search, but if cutoff is set to a value too low the model will become infeasible""" return self.solver.get_cutoff() @cutoff.setter def cutoff(self, cutoff: float): self.solver.set_cutoff(cutoff) @property def max_mip_gap_abs(self) -> float: """tolerance for the quality of the optimal solution, if a solution with cost :math:`c` and a lower bound :math:`l` are available and :math:`c-l<` :code:`mip_gap_abs`, the search will be concluded, see mip_gap to determine a percentage value """ return self.solver.get_mip_gap_abs() @max_mip_gap_abs.setter def max_mip_gap_abs(self, max_mip_gap_abs: float): self.solver.set_mip_gap(max_mip_gap_abs) @property def max_mip_gap(self) -> float: """value indicating the tolerance for the maximum percentage deviation from the optimal solution cost, if a solution with cost :math:`c` and a lower bound :math:`l` are available and :math:`(c-l)/l <` :code:`max_mip_gap` the search will be concluded.""" return self.solver.get_mip_gap() @max_mip_gap.setter def max_mip_gap(self, max_mip_gap: float): self.solver.set_mip_gap(max_mip_gap) @property def max_seconds(self) -> float: """time limit in seconds for search""" return self.solver.get_max_seconds() @max_seconds.setter def max_seconds(self, max_seconds: float): self.solver.set_max_seconds(max_seconds) @property def max_nodes(self) -> int: """maximum number of nodes to be explored in the search tree""" return self.solver.get_max_nodes() @max_nodes.setter def max_nodes(self, max_nodes: int): self.solver.set_max_nodes(max_nodes) @property def max_solutions(self) -> int: """solution limit, search will be stopped when :code:`max_solutions` were found""" return self.solver.get_max_solutions() @max_solutions.setter def max_solutions(self, max_solutions: int): self.solver.set_max_solutions(max_solutions) @property def status(self) -> OptimizationStatus: """ optimization status, which can be OPTIMAL(0), ERROR(-1), INFEASIBLE(1), UNBOUNDED(2). When optimizing problems with integer variables some additional cases may happen, FEASIBLE(3) for the case when a feasible solution was found but optimality was not proved, INT_INFEASIBLE(4) for the case when the lp relaxation is feasible but no feasible integer solution exists and NO_SOLUTION_FOUND(5) for the case when an integer solution was not found in the optimization. """ return self.__status def remove(self, objects): """removes variable(s) and/or constraint(s) from the model Args: objects: can be a Var, a Constr or a list of these objects """ if isinstance(objects, Var): self.solver.remove_vars([objects.idx]) elif isinstance(objects, Constr): self.solver.remove_constrs([objects.idx]) elif isinstance(objects, list): vlist = [] clist = [] for o in objects: if isinstance(o, Var): vlist.append(o.idx) elif isinstance(o, Constr): clist.append(o.idx) else: raise Exception( "Cannot handle removal of object of type " + type(o) + " from model.") if vlist: vlist.sort() self.solver.remove_vars(vlist) self.__n_cols -= len(vlist) if clist: clist.sort() self.solver.remove_constrs(clist) self.__n_rows -= len(clist)
class Model: def __init__(self, name: str = "", sense: str = MINIMIZE, solver_name: str = ''): # initializing variables with default values self.name: str = name self.sense: str = sense self.solver_name: str = solver_name self.solver: Solver = None # list of constraints and variables self.constrs: List[Constr] = [] self.vars: List[Var] = [] if solver_name.upper() == GUROBI: from mip.gurobi import SolverGurobi self.solver = SolverGurobi(self, name, sense) elif solver_name.upper() == CBC: from mip.cbc import SolverCbc self.solver = SolverCbc(self, name, sense) else: # search for the best solver available if gurobi.has_gurobi: from mip.gurobi import SolverGurobi self.solver = SolverGurobi(self, name, sense) elif cbc.has_cbc: from mip.cbc import SolverCbc self.solver = SolverCbc(self, name, sense) def __del__(self): if self.solver: del self.solver def __iadd__(self, other) -> 'Model': if isinstance(other, LinExpr): if len(other.sense) == 0: # adding objective function components self.set_objective(other) else: # adding constraint self.add_constr(other) elif isinstance(other, tuple): if isinstance(other[0], LinExpr) and isinstance(other[1], str): if len(other[0].sense) == 0: self.set_objective(other[0]) else: self.add_constr(other[0], other[1]) return self def add_var(self, name: str = "", lb: float = 0.0, ub: float = INF, obj: float = 0.0, type: str = CONTINUOUS, column: "Column" = None) -> "Var": if len(name.strip()) == 0: nc = self.solver.num_cols() name = 'C{:011d}'.format(nc) idx = self.solver.add_var(obj, lb, ub, type, column, name) self.vars.append(Var(self, idx, name)) return self.vars[-1] def add_constr(self, lin_expr: "LinExpr", name: str = "") -> Constr: if isinstance(lin_expr, bool): return None # empty constraint idx = self.solver.add_constr(lin_expr, name) self.constrs.append(Constr(self, idx, name)) return self.constrs[-1] def copy(self, solver_name: str = None) -> "Model": if not solver_name: solver_name = self.solver_name copy: Model = Model(self.name, self.sense, solver_name) # adding variables for v in self.vars: copy.add_var(name=v.name, lb=v.lb, ub=v.ub, obj=v.obj, type=v.type) # adding constraints for c in self.constrs: expr = c.expr # todo: make copy of constraint's lin_expr copy.add_constr(lin_expr=expr, name=c.name) # setting objective function's constant copy.set_objective_const(self.get_objective_const()) return copy def get_objective(self) -> LinExpr: return self.solver.get_objective() def get_objective_const(self) -> float: return self.solver.get_objective_const() def optimize(self, maxSeconds=inf, maxNodes=inf, maxSolutions=inf ) -> int: if maxSeconds != inf or maxNodes != inf or maxSolutions != inf: self.solver.set_processing_limits(maxSeconds, maxNodes, maxSolutions) self.solver.optimize() def get_objective_value(self) -> float: return self.solver.get_objective_value() def set_start(self, variables: List["Var"], values: List[float]): self.solver.set_start(variables, values) def set_objective(self, expr, sense: str = "") -> None: if isinstance(expr, int) or isinstance(expr, float): self.solver.set_objective(LinExpr([], [], expr)) elif isinstance(expr, Var): self.solver.set_objective(LinExpr([expr], [1])) elif isinstance(expr, LinExpr): self.solver.set_objective(expr, sense) def set_objective_const(self, const: float) -> None: return self.solver.set_objective_const(const) def write(self, path: str) -> None: self.solver.write(path) def read(self, path: str) -> None: self.solver.read(path) nCols = self.solver.num_cols() nRows = self.solver.num_rows() for i in range(nCols): self.vars.append(Var(self, i, self.solver.var_get_name(i))) for i in range(nRows): self.constrs.append(Constr(self, i, self.solver.constr_get_name(i))) @property def num_cols(self) -> int: return len(self.vars) @property def num_rows(self) -> int: return len(self.constrs)
class Model: """ Mixed Integer Programming Model This is the main class, providing methods for building, optimizing, querying optimization results and reoptimizing Mixed-Integer Programming Models. To check how models are created please see the examples included. """ def __init__(self, name: str = "", sense: str = MINIMIZE, solver_name: str = ""): """Model constructor Creates a Mixed-Integer Linear Programming Model. The default model optimization direction is Minimization. To store and optimize the model the MIP package automatically searches and connects in runtime to the dynamic library of some MIP solver installed on your computer, nowadays gurobi and cbc are supported. This solver is automatically selected, but you can force the selection of a specific solver with the parameter solver_name. Args: name (str): model name sense (str): MINIMIZATION ("MIN") or MAXIMIZATION ("MAX") solver_name: gurobi or cbc, searches for which solver is available if not informed """ # initializing variables with default values self.name = name self.solver_name = solver_name self.solver = None if "solver_name" in environ: solver_name = environ["solver_name"] if "solver_name".upper() in environ: solver_name = environ["solver_name".upper()] self.__mipStart = [] # list of constraints and variables self.constrs = [] self.constrs_by_name = {} self.vars = [] self.vars_by_name = {} self.cut_generators = [] if solver_name.upper() == GUROBI: from mip.gurobi import SolverGurobi self.solver = SolverGurobi(self, name, sense) elif solver_name.upper() == CBC: from mip.cbc import SolverCbc self.solver = SolverCbc(self, name, sense) else: # checking which solvers are available from mip import gurobi if gurobi.has_gurobi: from mip.gurobi import SolverGurobi self.solver = SolverGurobi(self, name, sense) self.solver_name = GUROBI else: from mip import cbc from mip.cbc import SolverCbc self.solver = SolverCbc(self, name, sense) self.solver_name = CBC self.sense = sense def __del__(self): if self.solver: del self.solver def __iadd__(self, other) -> "Model": if isinstance(other, LinExpr): if len(other.sense) == 0: # adding objective function components self.objective = other else: # adding constraint self.add_constr(other) elif isinstance(other, tuple): if isinstance(other[0], LinExpr) and isinstance(other[1], str): if len(other[0].sense) == 0: self.objective = other[0] else: self.add_constr(other[0], other[1]) return self def add_var(self, name: str = "", lb: float = 0.0, ub: float = INF, obj: float = 0.0, var_type: str = CONTINUOUS, column: "Column" = None) -> "Var": """ Creates a new variable Adds a new variable to the model. Args: name (str): variable name (optional) lb (float): variable lower bound, default 0.0 ub (float): variable upper bound, default infinity obj (float): coefficient of this variable in the objective function, default 0 var_type (str): CONTINUOUS ("C"), BINARY ("B") or INTEGER ("I") column (Column): constraints where this variable will appear, necessary \ only when constraints are already created in the model and a new \ variable will be created. Examples: To add a variable x which is continuous and greater or equal to zero to model m:: x = m.add_var() The following code creates a vector of binary variables x[0], ..., x[n-1] to model m:: x = [m.add_var(type=BINARY) for i in range(n)] """ if var_type == BINARY: lb = 0.0 ub = 1.0 if len(name.strip()) == 0: nc = self.solver.num_cols() name = "C{:011d}".format(nc) idx = self.solver.add_var(obj, lb, ub, var_type, column, name) self.vars.append(Var(self, idx, name)) self.vars_by_name[name] = self.vars[-1] return self.vars[-1] def add_constr(self, lin_expr: "LinExpr", name: str = "") -> Constr: """ Creates a new constraint (row) Adds a new constraint to the model Args: lin_expr (LinExpr): linear expression name (str): optional constraint name, used when saving model to\ lp or mps files Examples: The following code adds the constraint :math:`x_1 + x_2 \leq 1` (x1 and x2 should be created first using :func:`add_var<mip.model.Model.add_var>`):: m += x1 + x2 <= 1 Which is equivalent to:: m.add_constr( x1 + x2 <= 1 ) Summation expressions can be used also, to add the constraint \ :math:`\displaystyle \sum_{i=0}^{n-1} x_i = y` and name this \ constraint cons1:: m += xsum(x[i] for i in range(n)) == y, "cons1" """ if isinstance(lin_expr, bool): return None # empty constraint idx = self.solver.add_constr(lin_expr, name) self.constrs.append(Constr(self, idx, name)) self.constrs_by_name[name] = self.constrs[-1] return self.constrs[-1] def copy(self, solver_name: str = None) -> "Model": """ Creates a copy of the current model Args: solver_name(str): solver name (optional) Returns: Model: clone of current model """ if not solver_name: solver_name = self.solver_name copy = Model(self.name, self.sense, solver_name) # adding variables for v in self.vars: copy.add_var(name=v.name, lb=v.lb, ub=v.ub, obj=v.obj, var_type=v.var_type) # adding constraints for c in self.constrs: expr = c.expr # todo: make copy of constraint"s lin_expr copy.add_constr(lin_expr=expr, name=c.name) # setting objective function"s constant copy.objective_const = self.get_objective_const() return copy def get_constr_by_name(self, name: str) -> "Constr": """ Queries a constraint per name Args: name(str): constraint name Returns: Constr: constraint """ return self.constrs_by_name.get(name, None) @property def objective_bound(self) -> float: return self.solver.get_objective_bound() @property def objective(self) -> LinExpr: """LinExpr: Objective function of the problem The objective function of the problem as a linear expression. Examples: The following code adds all x variables x[0], ..., x[n-1], to the objective function of model m with weight w:: m.objective = xsum(w*x[i] for i in range(n)) A simpler way to define the objective function is the use of the model operator += :: m += xsum(w*x[i] for i in range(n)) Note that the only difference of adding a constraint is the lack of a sense and a rhs. """ return self.solver.get_objective() @objective.setter def objective(self, expr): if isinstance(expr, int) or isinstance(expr, float): self.solver.set_objective(LinExpr([], [], expr)) elif isinstance(expr, Var): self.solver.set_objective(LinExpr([expr], [1])) elif isinstance(expr, LinExpr): self.solver.set_objective(expr) @property def sense(self) -> str: """ The optimization sense Returns: str: the objective function sense, MINIMIZE (default) or (MAXIMIZE) """ return self.solver.get_objective_sense() @sense.setter def sense(self, sense: str): self.solver.set_objective_sense(sense) @property def objective_const(self) -> float: """ Returns the current constant part of the objective function float: the constant part in the objective function """ return self.solver.get_objective_const() @objective_const.setter def objective_const(self, const: float) -> None: self.solver.set_objective_const(const) @property def objective_value(self) -> float: """ Objective function value Returns: float: returns the objective function value of the solution found. """ return self.solver.get_objective_value() @property def num_solutions(self) -> int: """ Number of solutions found during the MIP search Returns: int: number of solutions stored in the solution pool """ return self.solver.get_num_solutions() @property def objective_values(self) -> List[float]: """ List of costs of all solutions in the solution pool Returns: List[float]: costs of all solutions stored in the solution pool as an array from 0 (the best solution) to num_solutions-1. """ return [float(self.solver.get_objective_value_i(i))\ for i in range(self.num_solutions)] def get_var_by_name(self, name) -> "Var": """ Searchers a variable by its name Returns: Var: a reference to a variable """ return self.vars_by_name.get(name, None) def relax(self): """ Relax integrality constraints of variables Changes the type of all integer and binary variables to continuous. Bounds are preserved. """ self.solver.relax() for v in self.vars: if v.var_type == BINARY or v.var_type == INTEGER: v.var_type = CONTINUOUS def add_cut_generator(self, cuts_generator: "CutsGenerator") -> None: """ Adds a cut generator Cut generators are called whenever a solution where one or more integer variables appear with continuous values. A cut generator will try to produce one or more inequalities to remove this fractional point. Args: cuts_generator : CutsGenerator """ self.cut_generators.append(cuts_generator) @property def emphasis(self) -> int: """int: defines the main objective of the search, if set to 1 (FEASIBILITY) then the search process will focus on try to find quickly feasible solutions and improving them; if set to 2 (OPTIMALITY) then the search process will try to find a provable optimal solution, procedures to further improve the lower bounds will be activated in this setting, this may increase the time to produce the first feasible solutions but will probably pay off in longer runs; the default option if 0, where a balance between optimality and feasibility is sought. """ return self.solver.get_emphasis() @emphasis.setter def emphasis(self, emph: int): self.solver.set_emphasis(emph) def optimize(self, branch_selector: "BranchSelector" = None, incumbent_updater: "IncumbentUpdater" = None, lazy_constrs_generator: "LazyConstrsGenerator" = None, max_seconds: float = inf, max_nodes: int = inf, max_solutions: int = inf) -> int: """ Optimizes current model Optimizes current model, optionally specifying processing limits. To optimize model m within a processing time limit of 300 seconds:: m.optimize(max_seconds=300) Args: branch_selector (BranchSelector): Callback to select branch (an object of a class inheriting from BranchSelector must be passed) cuts_generator (CutsGenerator): Callback to generate cuts (an object of a class inheriting from CutsGenerator must be passed) incumbent_updater (IncumbentUpdater): Callback to update incumbent solution (an object of a class inheriting from IncumbentUpdater must be passed) lazy_constrs_generator (LazyConstrsGenerator): Callback to include lazy generated constraints (an object of a class inheriting from LazyConstrsGenerator must be passed) max_seconds (float): Maximum runtime in seconds (default: inf) max_nodes (float): Maximum number of nodes (default: inf) max_solutions (float): Maximum number of solutions (default: inf) Returns: int: optimization status, which can be OPTIMAL(0), ERROR(-1), INFEASIBLE(1), UNBOUNDED(2). When optimizing problems with integer variables some additional cases may happen, FEASIBLE(3) for the case when a feasible solution was found but optimality was not proved, INT_INFEASIBLE(4) for the case when the lp relaxation is feasible but no feasible integer solution exists and NO_SOLUTION_FOUND(5) for the case when an integer solution was not found in the optimization. """ self.solver.set_callbacks(branch_selector, incumbent_updater, lazy_constrs_generator) self.solver.set_processing_limits(max_seconds, max_nodes, max_solutions) return self.solver.optimize() def read(self, path: str) -> None: """ Reads a MIP model Reads a MIP model in .lp or .mps file format. Args: path(str): file name """ self.solver.read(path) n_cols = self.solver.num_cols() n_rows = self.solver.num_rows() for i in range(n_cols): self.vars.append(Var(self, i, self.solver.var_get_name(i))) self.vars_by_name[self.vars[-1].name] = self.vars[-1] for i in range(n_rows): self.constrs.append(Constr(self, i, self.solver.constr_get_name(i))) self.constrs_by_name[self.constrs[-1].name] = self.constrs[-1] self.sense = self.solver.get_objective_sense() @property def start(self) -> List[Tuple["Var", float]]: """ Enter an initial feasible solution Enters an initial feasible solution. Only the main binary/integer decision variables. Auxiliary or continuous variables are automatically computed. Args: start_sol: list of tuples Var,float indicating non-zero variables and their values in the initial feasible solution """ return self.__mipStart @start.setter def start(self, start_sol: List[Tuple["Var", float]]): self.__mipStart = start_sol self.solver.set_start(start_sol) def write(self, path: str) -> None: """ Saves the the MIP model Args: path(str): file name Saves the the MIP model, use the extension ".lp" or ".mps" in the file name to specify the file format. """ self.solver.write(path) @property def num_cols(self) -> int: return len(self.vars) @property def num_rows(self) -> int: return len(self.constrs) @property def cutoff(self) -> float: """float: upper limit for the solution cost, solutions with cost > cutoff will be removed from the search space, a small cutoff value may significantly speedup the search, but if cutoff is set to a value too low the model will become infeasible""" return self.solver.get_cutoff() @cutoff.setter def cutoff(self, value: float): self.solver.set_cutoff(value) @property def mip_gap_abs(self) -> float: """float: tolerance for the quality of the optimal solution, if a solution with cost c and a lower bound l are available and c-l<mip_gap_abs, the search will be concluded, see mip_gap to determine a percentage value """ return self.solver.get_mip_gap_abs() @mip_gap_abs.setter def mip_gap_abs(self, value): self.solver.set_mip_gap(value) @property def mip_gap(self) -> float: """float: percentage indicating the tolerance for the maximum percentage deviation from the optimal solution cost, if a solution with cost c and a lower bound l are available and (c-l)/l < mip_gap the search will be concluded.""" return self.solver.get_mip_gap() @mip_gap.setter def mip_gap(self, value): self.solver.set_mip_gap(value) @property def max_seconds(self) -> float: """float: time limit in seconds for search""" return self.solver.get_max_seconds() @max_seconds.setter def max_seconds(self, max_seconds: float): self.solver.set_max_seconds(max_seconds) @property def max_nodes(self) -> int: """int: maximum number of nodes to be explored in the search tree""" return self.solver.get_max_nodes() @max_nodes.setter def max_nodes(self, max_nodes: int): self.solver.set_max_nodes(max_nodes) @property def max_solutions(self) -> int: """int: solution limit, search will be stopped when max_solutions were found""" return self.solver.get_max_solutions() @max_solutions.setter def max_solutions(self, max_solutions: int): self.solver.set_max_solutions(max_solutions)