1import logging 2from os import environ 3from os.path import isfile 4from typing import List, Tuple, Optional, Union, Dict, Any 5import numbers 6import mip 7from .version import version 8 9logger = logging.getLogger(__name__) 10 11try: 12 import numpy as np 13except ImportError: 14 np = None 15 logger.debug("Numpy not available", exc_info=True) 16 17 18class Model: 19 """Mixed Integer Programming Model 20 21 This is the main class, providing methods for building, optimizing, 22 querying optimization results and re-optimizing Mixed-Integer Programming 23 Models. 24 25 To check how models are created please see the 26 :ref:`examples <chapExamples>` included. 27 28 Attributes: 29 vars(mip.VarList): list of problem variables (:class:`~mip.Var`) 30 constrs(mip.ConstrList): list of constraints (:class:`~mip.Constr`) 31 32 Examples: 33 >>> from mip import Model, MAXIMIZE, CBC, INTEGER, OptimizationStatus 34 >>> model = Model(sense=MAXIMIZE, solver_name=CBC) 35 >>> x = model.add_var(name='x', var_type=INTEGER, lb=0, ub=10) 36 >>> y = model.add_var(name='y', var_type=INTEGER, lb=0, ub=10) 37 >>> model += x + y <= 10 38 >>> model.objective = x + y 39 >>> status = model.optimize(max_seconds=2) 40 >>> status == OptimizationStatus.OPTIMAL 41 True 42 """ 43 44 def __init__( 45 self: "Model", 46 name: str = "", 47 sense: str = mip.MINIMIZE, 48 solver_name: str = "", 49 solver: Optional[mip.Solver] = None, 50 ): 51 """Model constructor 52 53 Creates a Mixed-Integer Linear Programming Model. The default model 54 optimization direction is Minimization. To store and optimize the model 55 the MIP package automatically searches and connects in runtime to the 56 dynamic library of some MIP solver installed on your computer, nowadays 57 gurobi and cbc are supported. This solver is automatically selected, 58 but you can force the selection of a specific solver with the parameter 59 solver_name. 60 61 Args: 62 name (str): model name 63 sense (str): mip.MINIMIZATION ("MIN") or mip.MAXIMIZATION ("MAX") 64 solver_name(str): gurobi or cbc, searches for which 65 solver is available if not informed 66 solver(mip.Solver): a (:class:`~mip.Solver`) object; note that 67 if this argument is provided, solver_name will be ignored 68 """ 69 self._ownSolver = True 70 # initializing variables with default values 71 self.solver_name = solver_name 72 self.solver = solver # type: Optional[mip.Solver] 73 74 # reading solver_name from an environment variable (if applicable) 75 if not solver: 76 if not self.solver_name and "solver_name" in environ: 77 self.solver_name = environ["solver_name"] 78 if not self.solver_name and "solver_name".upper() in environ: 79 self.solver_name = environ["solver_name".upper()] 80 81 # creating a solver instance 82 if self.solver_name.upper() in ["GUROBI", "GRB"]: 83 import mip.gurobi 84 85 self.solver = mip.gurobi.SolverGurobi(self, name, sense) 86 elif self.solver_name.upper() == "CBC": 87 import mip.cbc 88 89 self.solver = mip.cbc.SolverCbc(self, name, sense) 90 else: 91 import mip.gurobi 92 93 if mip.gurobi.found: 94 95 self.solver = mip.gurobi.SolverGurobi(self, name, sense) 96 self.solver_name = mip.GUROBI 97 else: 98 import mip.cbc 99 100 self.solver = mip.cbc.SolverCbc(self, name, sense) 101 self.solver_name = mip.CBC 102 103 # list of constraints and variables 104 self.constrs = mip.ConstrList(self) 105 self.vars = mip.VarList(self) 106 107 self._status = mip.OptimizationStatus.LOADED 108 109 # initializing additional control variables 110 self.__cuts = -1 111 self.__cut_passes = -1 112 self.__clique = -1 113 self.__preprocess = -1 114 self.__cuts_generator = None 115 self.__lazy_constrs_generator = None 116 self.__start = None 117 self.__threads = 0 118 self.__lp_method = mip.LP_Method.AUTO 119 self.__n_cols = 0 120 self.__n_rows = 0 121 self.__gap = mip.INF 122 self.__store_search_progress_log = False 123 self.__plog = mip.ProgressLog() 124 self.__integer_tol = 1e-6 125 self.__infeas_tol = 1e-6 126 self.__opt_tol = 1e-6 127 self.__max_mip_gap = 1e-4 128 self.__max_mip_gap_abs = 1e-10 129 self.__seed = 0 130 self.__round_int_vars = True 131 self.__sol_pool_size = 10 132 self.__max_seconds_same_incumbent = mip.INF 133 self.__max_nodes_same_incumbent = mip.INF 134 135 def __del__(self: "Model"): 136 del self.solver 137 138 def _iadd_tensor_element( 139 self: "Model", 140 tensor: mip.LinExprTensor, 141 element: Union[mip.LinExpr, mip.CutPool, numbers.Real, bool], 142 index: Tuple[int, ...] = None, 143 label: str = None, 144 ): 145 # the tensor could contain LinExpr or constraints 146 if isinstance(element, mip.LinExpr) and element.sense == 0 and tensor.size > 1: 147 raise Exception("Only scalar objective functions are allowed") 148 149 # if the problem is sparse, it is common to have multiple boolean elements in constraints, we should ignore those 150 if isinstance(element, mip.LinExpr) or isinstance(element, mip.CutPool): 151 if index and label: 152 scalar_label = "%s_%s" % (label, ("_".join(map(str, index)))) 153 scalar = (element, scalar_label) 154 else: 155 scalar = element 156 157 self.__iadd__(scalar) 158 159 def __iadd__(self: "Model", other) -> "Model": 160 if isinstance(other, mip.LinExpr): 161 if len(other.sense) == 0: 162 # adding objective function components 163 self.objective = other 164 else: 165 # adding constraint 166 self.add_constr(other) 167 elif isinstance(other, tuple): 168 if len(other) == 2: 169 if isinstance(other[0], mip.LinExpr) and isinstance(other[1], str): 170 if len(other[0].sense) == 0: 171 self.objective = other[0] 172 else: 173 self.add_constr(other[0], other[1]) 174 elif isinstance(other[0], mip.LinExprTensor) and isinstance( 175 other[1], str 176 ): 177 if np is None: 178 raise ModuleNotFoundError( 179 "You need to install package numpy to use tensors" 180 ) 181 for index, element in np.ndenumerate(other[0]): 182 # add all elements of the tensor 183 self._iadd_tensor_element(other[0], element, index, other[1]) 184 else: 185 raise TypeError( 186 "tuple with types {} and {} not supported.".format( 187 type(other[0]), type(other[1]) 188 ) 189 ) 190 else: 191 raise TypeError("tuple with len {} not supported".format(len(other))) 192 elif isinstance(other, mip.CutPool): 193 for cut in other.cuts: 194 self.add_constr(cut) 195 elif isinstance(other, mip.LinExprTensor): 196 if np is None: 197 raise ModuleNotFoundError( 198 "You need to install package numpy to use tensors" 199 ) 200 for element in other.flat: 201 self._iadd_tensor_element(other, element) 202 else: 203 raise TypeError("type {} not supported".format(type(other))) 204 205 return self 206 207 def add_var( 208 self: "Model", 209 name: str = "", 210 lb: numbers.Real = 0.0, 211 ub: numbers.Real = mip.INF, 212 obj: numbers.Real = 0.0, 213 var_type: str = mip.CONTINUOUS, 214 column: "mip.Column" = None, 215 ) -> "mip.Var": 216 """Creates a new variable in the model, returning its reference 217 218 Args: 219 name (str): variable name (optional) 220 lb (numbers.Real): variable lower bound, default 0.0 221 ub (numbers.Real): variable upper bound, default infinity 222 obj (numbers.Real): coefficient of this variable in the objective 223 function, default 0 224 var_type (str): CONTINUOUS ("C"), BINARY ("B") or INTEGER ("I") 225 column (mip.Column): constraints where this variable will appear, 226 necessary only when constraints are already created in 227 the model and a new variable will be created. 228 229 Examples: 230 231 To add a variable :code:`x` which is continuous and greater or 232 equal to zero to model :code:`m`:: 233 234 x = m.add_var() 235 236 The following code adds a vector of binary variables 237 :code:`x[0], ..., x[n-1]` to the model :code:`m`:: 238 239 x = [m.add_var(var_type=BINARY) for i in range(n)] 240 241 :rtype: mip.Var 242 """ 243 return self.vars.add(name, lb, ub, obj, var_type, column) 244 245 def add_var_tensor( 246 self: "Model", shape: Tuple[int, ...], name: str, **kwargs 247 ) -> mip.LinExprTensor: 248 """Creates new variables in the model, arranging them in a numpy 249 tensor and returning its reference 250 251 Args: 252 shape (Tuple[int, ...]): shape of the numpy tensor 253 name (str): variable name 254 **kwargs: all other named arguments will be used as 255 :meth:`~mip.Model.add_var` arguments 256 257 Examples: 258 259 To add a tensor of variables :code:`x` with shape (3, 5) and which 260 is continuous in any variable and have all values greater or equal 261 to zero to model :code:`m`:: 262 263 x = m.add_var_tensor((3, 5), "x") 264 265 :rtype: mip.LinExprTensor 266 """ 267 if np is None: 268 raise ModuleNotFoundError( 269 "You need to install package numpy in order to use tensors" 270 ) 271 272 def _add_tensor(m, shape, name, **kwargs): 273 assert name is not None 274 assert len(shape) > 0 275 276 if len(shape) == 1: 277 return [ 278 m.add_var(name=("%s_%d" % (name, i)), **kwargs) 279 for i in range(shape[0]) 280 ] 281 return [ 282 _add_tensor(m, shape[1:], name=("%s_%d" % (name, i)), **kwargs) 283 for i in range(shape[0]) 284 ] 285 286 return np.array(_add_tensor(self, shape, name, **kwargs)).view(mip.LinExprTensor) 287 288 def add_constr( 289 self: "Model", 290 lin_expr: "mip.LinExpr", 291 name: str = "", 292 priority: "mip.constants.ConstraintPriority" = None, 293 ) -> "mip.Constr": 294 r"""Creates a new constraint (row). 295 296 Adds a new constraint to the model, returning its reference. 297 298 Args: 299 lin_expr(mip.LinExpr): linear expression 300 name(str): optional constraint name, used when saving model to 301 lp or mps files 302 priority(mip.constants.ConstraintPriority): optional constraint 303 priority 304 305 Examples: 306 307 The following code adds the constraint :math:`x_1 + x_2 \leq 1` 308 (x1 and x2 should be created first using 309 :meth:`~mip.Model.add_var`):: 310 311 m += x1 + x2 <= 1 312 313 Which is equivalent to:: 314 315 m.add_constr( x1 + x2 <= 1 ) 316 317 Summation expressions can be used also, to add the constraint \ 318 :math:`\displaystyle \sum_{i=0}^{n-1} x_i = y` and name this \ 319 constraint :code:`cons1`:: 320 321 m += xsum(x[i] for i in range(n)) == y, "cons1" 322 323 Which is equivalent to:: 324 325 m.add_constr( xsum(x[i] for i in range(n)) == y, "cons1" ) 326 327 :rtype: mip.Constr 328 """ 329 330 if isinstance(lin_expr, bool): 331 raise mip.InvalidLinExpr( 332 "A boolean (true/false) cannot be used as a constraint." 333 ) 334 # TODO: some tests use empty linear constraints, which ideally should not happen 335 # if len(lin_expr) == 0: 336 # raise mip.InvalidLinExpr( 337 # "An empty linear expression cannot be used as a constraint." 338 # ) 339 return self.constrs.add(lin_expr, name, priority) 340 341 def add_lazy_constr(self: "Model", expr: "mip.LinExpr"): 342 """Adds a lazy constraint 343 344 A lazy constraint is a constraint that is only inserted 345 into the model after the first integer solution that violates 346 it is found. When lazy constraints are used a restricted 347 pre-processing is executed since the complete model is not 348 available at the beginning. If the number of lazy constraints 349 is too large then they can be added during the search process 350 by implementing a 351 :class:`~mip.ConstrsGenerator` and setting the 352 property :attr:`~mip.Model.lazy_constrs_generator` of 353 :class:`Model`. 354 355 Args: 356 expr(mip.LinExpr): the linear constraint 357 """ 358 self.solver.add_lazy_constr(expr) 359 360 def add_sos(self: "Model", sos: List[Tuple["mip.Var", numbers.Real]], sos_type: int): 361 r"""Adds an Special Ordered Set (SOS) to the model 362 363 An explanation on Special Ordered Sets is provided :ref:`here <chapSOS>`. 364 365 366 Args: 367 sos(List[Tuple[Var, numbers.Real]]): 368 list including variables (not necessarily binary) and 369 respective weights in the model 370 sos_type(int): 371 1 for Type 1 SOS, where at most one of the binary 372 variables can be set to one and 2 for Type 2 SOS, where at 373 most two variables from the list may be selected. In type 374 2 SOS the two selected variables will be consecutive in 375 the list. 376 """ 377 self.solver.add_sos(sos, sos_type) 378 379 def clear(self: "Model"): 380 """Clears the model 381 382 All variables, constraints and parameters will be reset. In addition, 383 a new solver instance will be instantiated to implement the 384 formulation. 385 """ 386 # creating a new solver instance 387 sense = self.sense 388 389 if self.solver_name.upper() in ["GRB", "GUROBI"]: 390 import mip.gurobi 391 392 self.solver = mip.gurobi.SolverGurobi(self, self.name, sense) 393 elif self.solver_name.upper() == "CBC": 394 import mip.cbc 395 396 self.solver = mip.cbc.SolverCbc(self, self.name, sense) 397 else: 398 # checking which solvers are available 399 import mip.gurobi 400 401 if mip.gurobi.found: 402 self.solver = mip.gurobi.SolverGurobi(self, self.name, sense) 403 self.solver_name = mip.GUROBI 404 else: 405 import mip.cbc 406 407 self.solver = mip.cbc.SolverCbc(self, self.name, sense) 408 self.solver_name = mip.CBC 409 410 # list of constraints and variables 411 self.constrs = mip.ConstrList(self) 412 self.vars = mip.VarList(self) 413 414 # initializing additional control variables 415 self.__cuts = 1 416 self.__cuts_generator = None 417 self.__lazy_constrs_generator = None 418 self.__start = [] 419 self._status = mip.OptimizationStatus.LOADED 420 self.__threads = 0 421 422 def copy(self: "Model", solver_name: str = "") -> "Model": 423 """Creates a copy of the current model 424 425 Args: 426 solver_name(str): solver name (optional) 427 428 :rtype: Model 429 430 Returns: 431 clone of current model 432 """ 433 if not solver_name: 434 solver_name = self.solver_name 435 copy = Model(self.name, self.sense, solver_name) 436 437 # adding variables 438 for v in self.vars: 439 copy.add_var(name=v.name, lb=v.lb, ub=v.ub, obj=v.obj, var_type=v.var_type) 440 441 # adding constraints 442 for c in self.constrs: 443 orig_expr = c.expr 444 priority = c.priority 445 expr = mip.LinExpr(const=orig_expr.const, sense=orig_expr.sense) 446 for (var, value) in orig_expr.expr.items(): 447 expr.add_term(self.vars[var.idx], value) 448 copy.add_constr(lin_expr=expr, name=c.name, priority=priority) 449 450 # setting objective function"s constant 451 copy.objective_const = self.objective_const 452 453 return copy 454 455 def constr_by_name(self: "Model", name: str) -> Optional["mip.Constr"]: 456 """Queries a constraint by its name 457 458 Args: 459 name(str): constraint name 460 461 :rtype: Optional[mip.Constr] 462 463 Returns: 464 constraint or None if not found 465 """ 466 cidx = self.solver.constr_get_index(name) 467 if cidx < 0 or cidx > len(self.constrs): 468 return None 469 return self.constrs[cidx] 470 471 def var_by_name(self: "Model", name: str) -> Optional["mip.Var"]: 472 """Searchers a variable by its name 473 474 :rtype: Optional[mip.Var] 475 476 Returns: 477 Variable or None if not found 478 """ 479 v = self.solver.var_get_index(name) 480 if v < 0 or v > len(self.vars): 481 return None 482 return self.vars[v] 483 484 def clique_merge(self, constrs: Optional[List["mip.Constr"]] = None): 485 r"""This procedure searches for constraints with conflicting variables 486 and attempts to group these constraints in larger constraints with all 487 conflicts merged. 488 489 For example, if your model has the following constraints: 490 491 .. math:: 492 493 x_1 + x_2 \leq 1 494 495 x_2 + x_3 \leq 1 496 497 x_1 + x_3 \leq 1 498 499 Then they can all be removed and replaced by the stronger inequality: 500 501 .. math:: 502 503 x_1 + x_2 + x_3 \leq 1 504 505 Args: 506 constrs (Optional[List[mip.Constr]]): constraints that should be checked for 507 merging. All constraints will be checked if :code:`constrs` is None. 508 509 """ 510 self.solver.clique_merge(constrs) 511 self.constrs.update_constrs(self.solver.num_rows()) 512 513 def generate_cuts( 514 self: "Model", 515 cut_types: Optional[List["mip.CutType"]] = None, 516 depth: int = 0, 517 npass: int = 0, 518 max_cuts: int = 8192, 519 min_viol: float = 1e-4, 520 ) -> mip.CutPool: 521 """Tries to generate cutting planes for the current fractional 522 solution. To optimize only the linear programming relaxation and not 523 discard integrality information from variables you must call first 524 :code:`model.optimize(relax=True)`. 525 526 This method only works with the CBC mip solver, as Gurobi does not 527 supports calling only cut generators. 528 529 Args: 530 cut_types (List[CutType]): types of cuts that can be generated, if 531 an empty list is specified then all available cut generators 532 will be called. 533 depth: depth of the search tree, when informed the cut generator 534 may decide to generate more/less cuts depending on the depth. 535 max_cuts(int): cut separation will stop when at least max_cuts 536 violated cuts were found. 537 min_viol(float): cuts which are not violated by at least min_viol 538 will be discarded. 539 540 541 :rtype: mip.CutPool 542 """ 543 if self.status != mip.OptimizationStatus.OPTIMAL: 544 raise mip.SolutionNotAvailable() 545 546 return self.solver.generate_cuts(cut_types, depth, npass, max_cuts, min_viol) 547 548 @property 549 def conflict_graph(self: "Model") -> "mip.ConflictGraph": 550 """: Returns the :class:`~mip.ConflictGraph` of a MIP model. 551 552 :rtype: mip.ConflictGraph 553 """ 554 555 return mip.ConflictGraph(self) 556 557 def optimize( 558 self: "Model", 559 max_seconds: numbers.Real = mip.INF, 560 max_nodes: int = mip.INT_MAX, 561 max_solutions: int = mip.INT_MAX, 562 max_seconds_same_incumbent: numbers.Real = mip.INF, 563 max_nodes_same_incumbent: int = mip.INT_MAX, 564 relax: bool = False, 565 ) -> mip.OptimizationStatus: 566 """Optimizes current model 567 568 Optimizes current model, optionally specifying processing limits. 569 570 To optimize model :code:`m` within a processing time limit of 571 300 seconds:: 572 573 m.optimize(max_seconds=300) 574 575 Args: 576 max_seconds (numbers.Real): Maximum runtime in seconds (default: inf) 577 max_nodes (int): Maximum number of nodes (default: inf) 578 max_solutions (int): Maximum number of solutions (default: inf) 579 max_seconds_same_incumbent (numbers.Real): Maximum time in seconds 580 that the search can go on if a feasible solution is available 581 and it is not being improved 582 max_nodes_same_incumbent (int): Maximum number of nodes 583 that the search can go on if a feasible solution is available 584 and it is not being improved 585 relax (bool): if true only the linear programming relaxation will 586 be solved, i.e. integrality constraints will be temporarily 587 discarded. 588 589 Returns: 590 optimization status, which can be OPTIMAL(0), ERROR(-1), 591 INFEASIBLE(1), UNBOUNDED(2). When optimizing problems 592 with integer variables some additional cases may happen, 593 FEASIBLE(3) for the case when a feasible solution was found 594 but optimality was not proved, INT_INFEASIBLE(4) for the case 595 when the lp relaxation is feasible but no feasible integer 596 solution exists and NO_SOLUTION_FOUND(5) for the case when 597 an integer solution was not found in the optimization. 598 599 :rtype: mip.OptimizationStatus 600 601 """ 602 if not self.solver.num_cols(): 603 logger.warning("Model has no variables. Nothing to optimize.") 604 return mip.OptimizationStatus.OTHER 605 606 if self.__threads != 0: 607 self.solver.set_num_threads(self.__threads) 608 # self.solver.set_callbacks(branch_selector, 609 # incumbent_updater, lazy_constrs_generator) 610 self.solver.set_processing_limits( 611 max_seconds, 612 max_nodes, 613 max_solutions, 614 max_seconds_same_incumbent, 615 max_nodes_same_incumbent, 616 ) 617 618 self._status = self.solver.optimize(relax) 619 # has a solution and is a MIP 620 if self.num_solutions and self.num_int > 0: 621 best = self.objective_value 622 lb = self.objective_bound 623 if abs(best) <= 1e-10: 624 self.__gap = mip.INF 625 else: 626 self.__gap = abs(best - lb) / abs(best) 627 628 if self.store_search_progress_log: 629 self.__plog.log = self.solver.get_log() 630 self.__plog.instance = self.name 631 632 return self._status 633 634 def read(self: "Model", path: str): 635 """Reads a MIP model or an initial feasible solution. 636 637 One of the following file name extensions should be used 638 to define the contents of what will be loaded: 639 640 :code:`.lp` 641 mip model stored in the 642 `LP file format <https://www.ibm.com/support/knowledgecenter/SSSA5P_12.9.0/ilog.odms.cplex.help/CPLEX/GettingStarted/topics/tutorials/InteractiveOptimizer/usingLPformat.html>`_ 643 644 :code:`.mps` 645 mip model stored in the 646 `MPS file format <https://en.wikipedia.org/wiki/MPS_(format)>`_ 647 648 :code:`.sol` 649 initial integer feasible solution 650 651 :code:`.bas` 652 `optimal basis <http://lpsolve.sourceforge.net/5.5/bas-format.htm>`_ for the linear programming relaxation. 653 654 Note: if a new problem is readed, all variables, constraints 655 and parameters from the current model will be cleared. 656 657 Args: 658 path(str): file name 659 """ 660 if not isfile(path): 661 raise OSError(2, "File {} does not exists".format(path)) 662 663 if path.lower().endswith(".sol") or path.lower().endswith(".mst"): 664 mip_start = load_mipstart(path) 665 if not mip_start: 666 raise FileNotFoundError( 667 "File {} does not contains a valid feasible \ 668 solution.".format( 669 path 670 ) 671 ) 672 var_list = [] 673 for name, value in mip_start: 674 var = self.var_by_name(name) 675 if var is not None: 676 var_list.append((var, value)) 677 if not var_list: 678 raise ValueError( 679 "Invalid variable(s) name(s) in \ 680 mipstart file {}".format( 681 path 682 ) 683 ) 684 685 self.start = var_list 686 return 687 688 if path.lower().endswith(".bas"): 689 if self.num_cols == 0: 690 raise mip.ProgrammingError( 691 "Cannot load optimal LP basis for empty model." 692 ) 693 self.solver.read(path) 694 return 695 696 # reading model 697 model_ext = [".lp", ".mps", ".mps.gz"] 698 699 fn_low = path.lower() 700 for ext in model_ext: 701 if fn_low.endswith(ext): 702 self.clear() 703 self.solver.read(path) 704 self.vars.update_vars(self.solver.num_cols()) 705 self.constrs.update_constrs(self.solver.num_rows()) 706 return 707 708 raise ValueError( 709 "Use .lp, .mps, .sol or .mst as file extension \ 710 to indicate the file format." 711 ) 712 713 def relax(self: "Model"): 714 """Relax integrality constraints of variables 715 716 Changes the type of all integer and binary variables to 717 continuous. Bounds are preserved. 718 """ 719 self.solver.relax() 720 721 def write(self: "Model", file_path: str): 722 """Saves a MIP model or an initial feasible solution. 723 724 One of the following file name extensions should be used 725 to define the contents of what will be saved: 726 727 :code:`.lp` 728 mip model stored in the 729 `LP file format <https://www.ibm.com/support/knowledgecenter/SSSA5P_12.9.0/ilog.odms.cplex.help/CPLEX/GettingStarted/topics/tutorials/InteractiveOptimizer/usingLPformat.html>`_ 730 731 :code:`.mps` 732 mip model stored in the 733 `MPS file format <https://en.wikipedia.org/wiki/MPS_(format)>`_ 734 735 :code:`.sol` 736 initial feasible solution 737 738 :code:`.bas` 739 `optimal basis <http://lpsolve.sourceforge.net/5.5/bas-format.htm>`_ for the linear programming relaxation. 740 741 Args: 742 file_path(str): file name 743 """ 744 if file_path.lower().endswith(".sol") or file_path.lower().endswith(".mst"): 745 if self.start: 746 save_mipstart(self.start, file_path) 747 else: 748 mip_start = [(var, var.x) for var in self.vars if abs(var.x) >= 1e-8] 749 save_mipstart(mip_start, file_path) 750 elif ( 751 file_path.lower().endswith(".lp") 752 or file_path.lower().endswith(".mps") 753 or file_path.lower().endswith(".bas") 754 ): 755 self.solver.write(file_path) 756 else: 757 raise ValueError( 758 "Use .lp, .mps, .sol or .mst as file extension \ 759 to indicate the file format." 760 ) 761 762 @property 763 def objective_bound(self: "Model") -> Optional[numbers.Real]: 764 """:A valid estimate computed for the optimal solution cost, lower 765 bound in the case of minimization, equals to 766 :attr:`~mip.Model.objective_value` if the optimal solution was found. 767 """ 768 if self.status not in [ 769 mip.OptimizationStatus.OPTIMAL, 770 mip.OptimizationStatus.FEASIBLE, 771 mip.OptimizationStatus.NO_SOLUTION_FOUND, 772 ]: 773 return None 774 775 return self.solver.get_objective_bound() 776 777 @property 778 def name(self: "Model") -> str: 779 """:The problem (instance) name. 780 781 This name should be used to identify the instance that this model 782 refers, e.g.: productionPlanningMay19. This name is stored when 783 saving (:meth:`~mip.Model.write`) the model in :code:`.LP` 784 or :code:`.MPS` file formats. 785 """ 786 return self.solver.get_problem_name() 787 788 @name.setter 789 def name(self: "Model", name: str): 790 self.solver.set_problem_name(name) 791 792 @property 793 def objective(self: "Model") -> "mip.LinExpr": 794 """The objective function of the problem as a linear expression. 795 796 Examples: 797 798 The following code adds all :code:`x` variables :code:`x[0], 799 ..., x[n-1]`, to the objective function of model :code:`m` 800 with the same cost :code:`w`:: 801 802 m.objective = xsum(w*x[i] for i in range(n)) 803 804 A simpler way to define the objective function is the use of the 805 model operator += :: 806 807 m += xsum(w*x[i] for i in range(n)) 808 809 Note that the only difference of adding a constraint is the lack of 810 a sense and a rhs. 811 812 :rtype: mip.LinExpr 813 """ 814 return self.solver.get_objective() 815 816 @objective.setter 817 def objective( 818 self: "Model", 819 objective: Union[numbers.Real, "mip.Var", "mip.LinExpr", "mip.LinExprTensor"], 820 ): 821 if isinstance(objective, numbers.Real): 822 self.solver.set_objective(mip.LinExpr([], [], objective)) 823 elif isinstance(objective, mip.Var): 824 self.solver.set_objective(mip.LinExpr([objective], [1])) 825 elif isinstance(objective, mip.LinExpr): 826 self.solver.set_objective(objective) 827 elif isinstance(objective, mip.LinExprTensor): 828 if np is None: 829 raise ModuleNotFoundError( 830 "You need to install package numpy to use tensors" 831 ) 832 if objective.size != 1: 833 raise ValueError( 834 "objective set to tensor of shape {}, only scalars are allowed".format( 835 objective.shape 836 ) 837 ) 838 self.solver.set_objective(objective.flatten()[0]) 839 else: 840 raise TypeError("type {} not supported".format(type(objective))) 841 842 @property 843 def verbose(self: "Model") -> int: 844 """0 to disable solver messages printed on the screen, 1 to enable""" 845 return self.solver.get_verbose() 846 847 @verbose.setter 848 def verbose(self: "Model", verbose: int): 849 self.solver.set_verbose(verbose) 850 851 @property 852 def lp_method(self: "Model") -> mip.LP_Method: 853 """Which method should be used to solve the linear programming 854 problem. If the problem has integer variables that this affects only 855 the solution of the first linear programming relaxation. 856 857 :rtype: mip.LP_Method 858 """ 859 return self.__lp_method 860 861 @lp_method.setter 862 def lp_method(self: "Model", lpm: mip.LP_Method): 863 self.__lp_method = lpm 864 865 @property 866 def threads(self: "Model") -> int: 867 r"""number of threads to be used when solving the problem. 868 0 uses solver default configuration, -1 uses the number of available 869 processing cores and :math:`\geq 1` uses the specified number of 870 threads. An increased number of threads may improve the solution 871 time but also increases the memory consumption.""" 872 return self.__threads 873 874 @threads.setter 875 def threads(self: "Model", threads: int): 876 self.__threads = threads 877 878 @property 879 def sense(self: "Model") -> str: 880 """The optimization sense 881 882 Returns: 883 the objective function sense, MINIMIZE (default) or (MAXIMIZE) 884 """ 885 886 return self.solver.get_objective_sense() 887 888 @sense.setter 889 def sense(self: "Model", sense: str): 890 self.solver.set_objective_sense(sense) 891 892 @property 893 def objective_const(self: "Model") -> float: 894 """Returns the constant part of the objective function""" 895 return self.solver.get_objective_const() 896 897 @objective_const.setter 898 def objective_const(self: "Model", objective_const: float): 899 self.solver.set_objective_const(objective_const) 900 901 @property 902 def objective_value(self: "Model") -> Optional[numbers.Real]: 903 """Objective function value of the solution found or None 904 if model was not optimized 905 """ 906 return self.solver.get_objective_value() 907 908 @property 909 def gap(self: "Model") -> float: 910 r""" 911 The optimality gap considering the cost of the best solution found 912 (:attr:`~mip.Model.objective_value`) 913 :math:`b` and the best objective bound :math:`l` 914 (:attr:`~mip.Model.objective_bound`) :math:`g` is 915 computed as: :math:`g=\\frac{|b-l|}{|b|}`. 916 If no solution was found or if :math:`b=0` then :math:`g=\infty`. 917 If the optimal solution was found then :math:`g=0`. 918 """ 919 return self.__gap 920 921 @property 922 def search_progress_log(self: "Model") -> mip.ProgressLog: 923 """:Log of bound improvements in the search. The output of MIP 924 solvers is a sequence of improving incumbent solutions (primal bound) 925 and estimates for the optimal cost (dual bound). When the costs of 926 these two bounds match the search is concluded. In truncated searches, 927 the most common situation for hard problems, at the end of the search 928 there is a :attr:`~mip.Model.gap` between these bounds. This property 929 stores the detailed events of improving these bounds during the search 930 process. Analyzing the evolution of these bounds you can see if you 931 need to improve your solver w.r.t. the production of feasible 932 solutions, by including an heuristic to produce a better initial 933 feasible solution, for example, or improve the formulation with cutting 934 planes, for example, to produce better dual bounds. To enable storing 935 the :attr:`~mip.Model.search_progress_log` set 936 :attr:`~mip.Model.store_search_progress_log` to True. 937 938 :rtype: mip.ProgressLog 939 """ 940 941 return self.__plog 942 943 @property 944 def store_search_progress_log(self: "Model") -> bool: 945 """ 946 Wether :attr:`~mip.Model.search_progress_log` will be stored 947 or not when optimizing. Default False. Activate it if you want to 948 analyze bound improvements over time.""" 949 return self.__store_search_progress_log 950 951 @store_search_progress_log.setter 952 def store_search_progress_log(self: "Model", store: bool): 953 self.__store_search_progress_log = store 954 955 # def plot_bounds_evolution(self): 956 # import matplotlib.pyplot as plt 957 # log = self.search_progress_log 958 # 959 # # plotting lower bound 960 # x = [a[0] for a in log] 961 # y = [a[1][0] for a in log] 962 # plt.plot(x, y) 963 # # plotting upper bound 964 # x = [a[0] for a in log if a[1][1] < 1e+50] 965 # y = [a[1][1] for a in log if a[1][1] < 1e+50] 966 # plt.plot(x, y) 967 # plt.show() 968 969 @property 970 def num_solutions(self: "Model") -> int: 971 """Number of solutions found during the MIP search 972 973 Returns: 974 number of solutions stored in the solution pool 975 976 """ 977 return self.solver.get_num_solutions() 978 979 @property 980 def objective_values(self: "Model") -> List[numbers.Real]: 981 """List of costs of all solutions in the solution pool 982 983 Returns: 984 costs of all solutions stored in the solution pool 985 as an array from 0 (the best solution) to 986 :attr:`~mip.Model.num_solutions`-1. 987 """ 988 return [self.solver.get_objective_value_i(i) for i in range(self.num_solutions)] 989 990 @property 991 def cuts_generator(self: "Model") -> Optional["mip.ConstrsGenerator"]: 992 """A cuts generator is an :class:`~mip.ConstrsGenerator` 993 object that receives a fractional solution and tries to generate one or 994 more constraints (cuts) to remove it. The cuts generator is called in 995 every node of the branch-and-cut tree where a solution that violates 996 the integrality constraint of one or more variables is found. 997 998 :rtype: Optional[mip.ConstrsGenerator] 999 """ 1000 1001 return self.__cuts_generator 1002 1003 @cuts_generator.setter 1004 def cuts_generator(self: "Model", cuts_generator: Optional["mip.ConstrsGenerator"]): 1005 self.__cuts_generator = cuts_generator 1006 1007 @property 1008 def lazy_constrs_generator( 1009 self: "Model", 1010 ) -> Optional["mip.ConstrsGenerator"]: 1011 """A lazy constraints generator is an 1012 :class:`~mip.ConstrsGenerator` object that receives 1013 an integer solution and checks its feasibility. If 1014 the solution is not feasible then one or more constraints can be 1015 generated to remove it. When a lazy constraints generator is informed 1016 it is assumed that the initial formulation is incomplete. Thus, a 1017 restricted pre-processing routine may be applied. If the initial 1018 formulation is incomplete, it may be interesting to use the same 1019 :class:`~mip.ConstrsGenerator` to generate cuts *and* lazy 1020 constraints. The use of *only* lazy constraints may be useful then 1021 integer solutions rarely violate these constraints. 1022 1023 :rtype: Optional[mip.ConstrsGenerator] 1024 """ 1025 1026 return self.__lazy_constrs_generator 1027 1028 @lazy_constrs_generator.setter 1029 def lazy_constrs_generator( 1030 self: "Model", lazy_constrs_generator: Optional["mip.ConstrsGenerator"] 1031 ): 1032 self.__lazy_constrs_generator = lazy_constrs_generator 1033 1034 @property 1035 def emphasis(self: "Model") -> "mip.SearchEmphasis": 1036 """defines the main objective of the search, if set to 1 (FEASIBILITY) 1037 then the search process will focus on try to find quickly feasible 1038 solutions and improving them; if set to 2 (OPTIMALITY) then the 1039 search process will try to find a provable optimal solution, 1040 procedures to further improve the lower bounds will be activated in 1041 this setting, this may increase the time to produce the first 1042 feasible solutions but will probably pay off in longer runs; 1043 the default option if 0, where a balance between optimality and 1044 feasibility is sought. 1045 1046 :rtype: mip.SearchEmphasis 1047 1048 """ 1049 return self.solver.get_emphasis() 1050 1051 @emphasis.setter 1052 def emphasis(self: "Model", emphasis: mip.SearchEmphasis): 1053 self.solver.set_emphasis(emphasis) 1054 1055 @property 1056 def preprocess(self: "Model") -> int: 1057 """Enables/disables pre-processing. Pre-processing tries to improve 1058 your MIP formulation. -1 means automatic, 0 means off and 1 1059 means on.""" 1060 return self.__preprocess 1061 1062 @preprocess.setter 1063 def preprocess(self: "Model", prep: int): 1064 self.__preprocess = prep 1065 1066 @property 1067 def pump_passes(self: "Model") -> int: 1068 """Number of passes of the Feasibility Pump [FGL05]_ heuristic. 1069 You may increase this value if you are not getting feasible 1070 solutions.""" 1071 return self.solver.get_pump_passes() 1072 1073 @pump_passes.setter 1074 def pump_passes(self: "Model", passes: int): 1075 self.solver.set_pump_passes(passes) 1076 1077 @property 1078 def cuts(self: "Model") -> int: 1079 """Controls the generation of cutting planes, -1 means automatic, 0 1080 disables completely, 1 (default) generates cutting planes in a moderate 1081 way, 2 generates cutting planes aggressively and 3 generates even more 1082 cutting planes. Cutting planes usually improve the LP relaxation bound 1083 but also make the solution time of the LP relaxation larger, so the 1084 overall effect is hard to predict and experimenting different values 1085 for this parameter may be beneficial.""" 1086 1087 return self.__cuts 1088 1089 @cuts.setter 1090 def cuts(self: "Model", gencuts: int): 1091 self.__cuts = gencuts 1092 1093 @property 1094 def cut_passes(self: "Model") -> int: 1095 """Maximum number of rounds of cutting planes. You may set this 1096 parameter to low values if you see that a significant amount of 1097 time is being spent generating cuts without any improvement in 1098 the lower bound. -1 means automatic, values greater than zero 1099 specify the maximum number of rounds.""" 1100 return self.__cut_passes 1101 1102 @cut_passes.setter 1103 def cut_passes(self: "Model", cp: int): 1104 self.__cut_passes = cp 1105 1106 @property 1107 def clique(self: "Model") -> int: 1108 """Controls the generation of clique cuts. -1 means automatic, 1109 0 disables it, 1 enables it and 2 enables more aggressive clique 1110 generation.""" 1111 return self.__clique 1112 1113 @clique.setter 1114 def clique(self: "Model", clq: int): 1115 self.__clique = clq 1116 1117 @property 1118 def start(self: "Model") -> Optional[List[Tuple["mip.Var", numbers.Real]]]: 1119 """Initial feasible solution 1120 1121 Enters an initial feasible solution. Only the main binary/integer 1122 decision variables which appear with non-zero values in the initial 1123 feasible solution need to be informed. Auxiliary or continuous 1124 variables are automatically computed. 1125 1126 :rtype: Optional[List[Tuple[mip.Var, numbers.Real]]] 1127 """ 1128 return self.__start 1129 1130 @start.setter 1131 def start(self: "Model", start: Optional[List[Tuple["mip.Var", numbers.Real]]]): 1132 self.__start = start 1133 if start is not None: 1134 self.solver.set_start(start) 1135 1136 def validate_mip_start(self: "Model"): 1137 """Validates solution entered in MIPStart 1138 1139 If the solver engine printed messages indicating that the initial 1140 feasible solution that you entered in :attr:`~mip.Model.start` is not 1141 valid then you can call this method to help discovering which set of 1142 variables is causing infeasibility. The current version is quite 1143 simple: the model is relaxed and one variable entered in mipstart is 1144 fixed per iteration, indicating if the model still feasible or not. 1145 """ 1146 1147 logger.info("Checking feasibility of MIPStart") 1148 mc = self.copy() 1149 mc.verbose = 0 1150 mc.relax() 1151 mc.optimize() 1152 if mc.status == mip.OptimizationStatus.INFEASIBLE: 1153 logger.info("Model is infeasible.\n") 1154 return 1155 if mc.status == mip.OptimizationStatus.UNBOUNDED: 1156 logger.info( 1157 "Model is unbounded. You probably need to insert " 1158 "additional constraints or bounds in variables." 1159 ) 1160 return 1161 if mc.status != mip.OptimizationStatus.OPTIMAL: 1162 logger.warning( 1163 "Unexpected status while optimizing LP relaxation:" 1164 " {}".format(mc.status) 1165 ) 1166 1167 logger.info("Model LP relaxation bound is {}".format(mc.objective_value)) 1168 1169 for (var, value) in self.start: 1170 logger.info("\tfixing %s to %g ... " % (var.name, value)) 1171 mc += var == value 1172 mc.optimize() 1173 if mc.status == mip.OptimizationStatus.OPTIMAL: 1174 logger.info("ok, obj now: {}".format(mc.objective_value)) 1175 else: 1176 logger.warning("NOT OK, optimization status: {}".format(mc.status)) 1177 return 1178 1179 logger.info( 1180 "Linear Programming relaxation of model with fixations from " 1181 "MIPStart is feasible." 1182 ) 1183 logger.info("MIP model may still be infeasible.") 1184 1185 @property 1186 def num_cols(self: "Model") -> int: 1187 """number of columns (variables) in the model""" 1188 return len(self.vars) 1189 1190 @property 1191 def num_int(self: "Model") -> int: 1192 """number of integer variables in the model""" 1193 return self.solver.num_int() 1194 1195 @property 1196 def num_rows(self: "Model") -> int: 1197 """number of rows (constraints) in the model""" 1198 return len(self.constrs) 1199 1200 @property 1201 def num_nz(self: "Model") -> int: 1202 """number of non-zeros in the constraint matrix""" 1203 return self.solver.num_nz() 1204 1205 @property 1206 def cutoff(self: "Model") -> numbers.Real: 1207 """upper limit for the solution cost, solutions with cost > cutoff 1208 will be removed from the search space, a small cutoff value may 1209 significantly speedup the search, but if cutoff is set to a value too 1210 low the model will become infeasible""" 1211 return self.solver.get_cutoff() 1212 1213 @cutoff.setter 1214 def cutoff(self: "Model", cutoff: float): 1215 self.solver.set_cutoff(cutoff) 1216 1217 @property 1218 def integer_tol(self: "Model") -> float: 1219 """Maximum distance to the nearest integer for a variable to be 1220 considered with an integer value. Default value: 1e-6. Tightening this 1221 value can increase the numerical precision but also probably increase 1222 the running time. As floating point computations always involve some 1223 loss of precision, values too close to zero will likely render some 1224 models impossible to optimize.""" 1225 return self.__integer_tol 1226 1227 @integer_tol.setter 1228 def integer_tol(self: "Model", int_tol: float): 1229 self.__integer_tol = int_tol 1230 1231 @property 1232 def infeas_tol(self: "Model") -> float: 1233 """Maximum allowed violation for constraints. 1234 1235 Default value: 1e-6. Tightening this value can increase the numerical 1236 precision but also probably increase the running time. As floating 1237 point computations always involve some loss of precision, values too 1238 close to zero will likely render some models impossible to optimize.""" 1239 1240 return self.__infeas_tol 1241 1242 @infeas_tol.setter 1243 def infeas_tol(self: "Model", inf_tol: float): 1244 self.__infeas_tol = inf_tol 1245 1246 @property 1247 def opt_tol(self: "Model") -> float: 1248 """Maximum reduced cost value for a solution of the LP relaxation to be 1249 considered optimal. Default value: 1e-6. Tightening this value can 1250 increase the numerical precision but also probably increase the running 1251 time. As floating point computations always involve some loss of 1252 precision, values too close to zero will likely render some models 1253 impossible to optimize.""" 1254 return self.__opt_tol 1255 1256 @opt_tol.setter 1257 def opt_tol(self: "Model", tol: float): 1258 self.__opt_tol = tol 1259 1260 @property 1261 def max_mip_gap_abs(self: "Model") -> float: 1262 """Tolerance for the quality of the optimal solution, if a solution 1263 with cost :math:`c` and a lower bound :math:`l` are available and 1264 :math:`c-l<` :code:`mip_gap_abs`, the search will be concluded, see 1265 :attr:`~mip.Model.max_mip_gap` to determine a percentage value. 1266 Default value: 1e-10.""" 1267 return self.__max_mip_gap_abs 1268 1269 @max_mip_gap_abs.setter 1270 def max_mip_gap_abs(self: "Model", max_mip_gap_abs: float): 1271 self.__max_mip_gap_abs = max_mip_gap_abs 1272 1273 @property 1274 def max_mip_gap(self: "Model") -> float: 1275 """value indicating the tolerance for the maximum percentage deviation 1276 from the optimal solution cost, if a solution with cost :math:`c` and 1277 a lower bound :math:`l` are available and 1278 :math:`(c-l)/l <` :code:`max_mip_gap` the search will be concluded. 1279 Default value: 1e-4.""" 1280 return self.__max_mip_gap 1281 1282 @max_mip_gap.setter 1283 def max_mip_gap(self: "Model", max_mip_gap: float): 1284 self.__max_mip_gap = max_mip_gap 1285 1286 @property 1287 def max_seconds(self: "Model") -> float: 1288 """time limit in seconds for search""" 1289 return self.solver.get_max_seconds() 1290 1291 @max_seconds.setter 1292 def max_seconds(self: "Model", max_seconds: float): 1293 self.solver.set_max_seconds(max_seconds) 1294 1295 @property 1296 def max_nodes(self: "Model") -> int: 1297 """maximum number of nodes to be explored in the search tree""" 1298 return self.solver.get_max_nodes() 1299 1300 @max_nodes.setter 1301 def max_nodes(self: "Model", max_nodes: int): 1302 self.solver.set_max_nodes(max_nodes) 1303 1304 @property 1305 def max_solutions(self: "Model") -> int: 1306 """solution limit, search will be stopped when :code:`max_solutions` 1307 were found""" 1308 return self.solver.get_max_solutions() 1309 1310 @max_solutions.setter 1311 def max_solutions(self: "Model", max_solutions: int): 1312 self.solver.set_max_solutions(max_solutions) 1313 1314 @property 1315 def seed(self: "Model") -> int: 1316 """Random seed. Small changes in the first decisions while solving the LP 1317 relaxation and the MIP can have a large impact in the performance, 1318 as discussed in [Fisch14]_. This behaviour can be exploited with multiple 1319 independent runs with different random seeds.""" 1320 1321 return self.__seed 1322 1323 @seed.setter 1324 def seed(self: "Model", seed: int): 1325 self.__seed = seed 1326 1327 @property 1328 def round_int_vars(self: "Model") -> bool: 1329 """MIP solvers perform computations using *limited precision* arithmetic. 1330 Thus a variable with value 0 may appear in the solution as 1331 0.000000000001. Thus, comparing this var to zero would return false. 1332 The safest approach would be to use something like abs(v.x) < 1e-7. 1333 To simplify code the solution value of integer variables can be 1334 automatically rounded to the nearest integer and then, comparisons like 1335 v.x == 0 would work. Rounding is not always a good idea specially in 1336 models with numerical instability, since it can increase the 1337 infeasibilities.""" 1338 1339 return self.__round_int_vars 1340 1341 @round_int_vars.setter 1342 def round_int_vars(self: "Model", round_iv: bool): 1343 self.__round_int_vars = round_iv 1344 1345 @property 1346 def sol_pool_size(self: "Model") -> int: 1347 1348 """Maximum number of solutions that will be stored during the search. 1349 To check how many solutions were found during the search use 1350 :meth:`~mip.Model.num_solutions`.""" 1351 1352 return self.__sol_pool_size 1353 1354 @sol_pool_size.setter 1355 def sol_pool_size(self: "Model", sol_pool_size: int): 1356 if sol_pool_size < 1: 1357 raise ValueError("Pool size must be at least one.") 1358 self.__sol_pool_size = sol_pool_size 1359 1360 @property 1361 def status(self: "Model") -> mip.OptimizationStatus: 1362 """optimization status, which can be OPTIMAL(0), ERROR(-1), 1363 INFEASIBLE(1), UNBOUNDED(2). When optimizing problems 1364 with integer variables some additional cases may happen, FEASIBLE(3) 1365 for the case when a feasible solution was found but optimality was 1366 not proved, INT_INFEASIBLE(4) for the case when the lp relaxation is 1367 feasible but no feasible integer solution exists and 1368 NO_SOLUTION_FOUND(5) for the case when an integer solution was not 1369 found in the optimization. 1370 1371 :rtype: mip.OptimizationStatus 1372 """ 1373 return self._status 1374 1375 def add_cut(self: "Model", cut: "mip.LinExpr"): 1376 1377 """Adds a violated inequality (cutting plane) to the linear programming 1378 model. If called outside the cut callback performs exactly as 1379 :meth:`~mip.Model.add_constr`. When called inside the cut 1380 callback the cut is included in the solver's cut pool, which will later 1381 decide if this cut should be added or not to the model. Repeated cuts, 1382 or cuts which will probably be less effective, e.g. with a very small 1383 violation, can be discarded. 1384 1385 Args: 1386 cut(mip.LinExpr): violated inequality 1387 """ 1388 self.solver.add_cut(cut) 1389 1390 def remove( 1391 self: "Model", 1392 objects: Union[mip.Var, mip.Constr, List[Union["mip.Var", "mip.Constr"]]], 1393 ): 1394 """removes variable(s) and/or constraint(s) from the model 1395 1396 Args: 1397 objects (Union[mip.Var, mip.Constr, List[Union[mip.Var, mip.Constr]]]): 1398 can be a :class:`~mip.Var`, a :class:`~mip.Constr` or a list of these objects 1399 """ 1400 if isinstance(objects, (mip.Var, mip.Constr)): 1401 objects = [objects] 1402 1403 if isinstance(objects, list): 1404 vlist = [] 1405 clist = [] 1406 for o in objects: 1407 if isinstance(o, mip.Var): 1408 vlist.append(o) 1409 elif isinstance(o, mip.Constr): 1410 clist.append(o) 1411 else: 1412 raise TypeError( 1413 "Cannot handle removal of object of type " 1414 "{} from model".format(type(o)) 1415 ) 1416 if vlist: 1417 self.vars.remove(vlist) 1418 if clist: 1419 self.constrs.remove(clist) 1420 else: 1421 raise TypeError( 1422 "Cannot handle removal of object of type " 1423 + type(objects) 1424 + " from model." 1425 ) 1426 1427 def translate(self: "Model", ref) -> Union[List[Any], Dict[Any, Any], "mip.Var"]: 1428 """Translates references of variables/containers of variables 1429 from another model to this model. Can be used to translate 1430 references of variables in the original model to references 1431 of variables in the pre-processed model. 1432 1433 :rtype: Union[List[Any], Dict[Any, Any], mip.Var] 1434 """ 1435 1436 res = None # type: Union[List[Any], Dict[Any, Any], Var] 1437 1438 if isinstance(ref, mip.Var): 1439 return self.var_by_name(ref.name) 1440 if isinstance(ref, list): 1441 res = list() 1442 for el in ref: 1443 res.append(self.translate(el)) 1444 return res 1445 if isinstance(ref, dict): 1446 res = dict() 1447 for key, value in ref.items(): 1448 res[key] = self.translate(value) 1449 return res 1450 1451 return ref 1452 1453 def check_optimization_results(self): 1454 """Checks the consistency of the optimization results, i.e., if the 1455 solution(s) produced by the MIP solver respect all constraints and 1456 variable values are within acceptable bounds and are integral when 1457 requested.""" 1458 if self.status in [ 1459 mip.OptimizationStatus.FEASIBLE, 1460 mip.OptimizationStatus.OPTIMAL, 1461 ]: 1462 assert self.num_solutions >= 1 1463 if self.num_solutions or self.status in [ 1464 mip.OptimizationStatus.FEASIBLE, 1465 mip.OptimizationStatus.OPTIMAL, 1466 ]: 1467 if self.sense == mip.MINIMIZE: 1468 assert self.objective_bound <= self.objective_value + 1e-10 1469 else: 1470 assert self.objective_bound + 1e-10 >= self.objective_value 1471 1472 for c in self.constrs: 1473 if c.expr.violation >= self.infeas_tol + self.infeas_tol * 0.1: 1474 raise mip.InfeasibleSolution( 1475 "Constraint {}:\n{}\n is violated." 1476 "Computed violation is {}." 1477 "Tolerance for infeasibility is {}." 1478 "Solution status is {}.".format( 1479 c.name, 1480 str(c), 1481 c.expr.violation, 1482 self.infeas_tol, 1483 self.status, 1484 ) 1485 ) 1486 for v in self.vars: 1487 if ( 1488 v.x <= v.lb - self.infeas_tol - 1e-20 1489 or v.x >= v.ub + self.infeas_tol + 1e-20 1490 ): 1491 raise mip.InfeasibleSolution( 1492 "Invalid solution value for " 1493 "variable {}={} variable bounds" 1494 " are [{}, {}].".format(v.name, v.x, v.lb, v.ub) 1495 ) 1496 if v.var_type in [mip.BINARY, mip.INTEGER]: 1497 if (round(v.x) - v.x) >= self.integer_tol + self.integer_tol * 0.1: 1498 raise mip.InfeasibleSolution( 1499 "Variable {}={} should be integral.".format(v.name, v.x) 1500 ) 1501 1502 1503def maximize(objective: Union["mip.LinExpr", "mip.Var"]) -> "mip.LinExpr": 1504 """ 1505 Function that should be used to set the objective function to MAXIMIZE 1506 a given linear expression (passed as argument). 1507 1508 Args: 1509 objective(Union[mip.LinExpr, Var]): linear expression 1510 1511 :rtype: mip.LinExpr 1512 """ 1513 if isinstance(objective, mip.Var): 1514 objective = mip.LinExpr([objective], [1.0]) 1515 objective.sense = mip.MAXIMIZE 1516 return objective 1517 1518 1519def minimize(objective: Union["mip.LinExpr", "mip.Var"]) -> "mip.LinExpr": 1520 """ 1521 Function that should be used to set the objective function to MINIMIZE 1522 a given linear expression (passed as argument). 1523 1524 Args: 1525 objective(Union[mip.LinExpr, Var]): linear expression 1526 1527 :rtype: mip.LinExpr 1528 """ 1529 if isinstance(objective, mip.Var): 1530 objective = mip.LinExpr([objective], [1.0]) 1531 objective.sense = mip.MINIMIZE 1532 return objective 1533 1534 1535def xsum(terms) -> "mip.LinExpr": 1536 """ 1537 Function that should be used to create a linear expression from a 1538 summation. While the python function sum() can also be used, this 1539 function is optimized version for quickly generating the linear 1540 expression. 1541 1542 Args: 1543 terms: set (ideally a list) of terms to be summed 1544 1545 :rtype: mip.LinExpr 1546 """ 1547 result = mip.LinExpr() 1548 for term in terms: 1549 result.add_term(term) 1550 return result 1551 1552 1553def compute_features(model: "Model") -> List[float]: 1554 """This function computes instance features for a MIP. Features are 1555 instance characteristics, such as number of columns, rows, matrix density, 1556 etc. These features can be used in machine learning algorithms to recommend 1557 parameter settings. To check names of features that are computed in this 1558 vector use :py:meth:`~mip.features` 1559 1560 Arguments: 1561 model(Model): the MIP model were features will be extracted 1562 """ 1563 return model.solver.feature_values() 1564 1565 1566def features() -> List[str]: 1567 """This function returns the list of problem feature names that can be 1568 computed :py:meth:`~mip.compute_features` 1569 """ 1570 import mip.cbc 1571 1572 return mip.cbc.feature_names() 1573 1574 1575# function aliases 1576quicksum = xsum 1577 1578 1579def save_mipstart(sol: List[Tuple["mip.Var", numbers.Real]], file_name: str, obj=0.0): 1580 """Saves a solution in a MIPStart (MST) file.""" 1581 f = open(file_name, "w") 1582 f.write("Feasible solution - objective {}\n".format(obj)) 1583 for i, (var, val) in enumerate(sol): 1584 f.write("{} {} {} {}\n".format(i, var.name, val, var.obj)) 1585 f.close() 1586 1587 1588def load_mipstart(file_name: str) -> List[Tuple[str, numbers.Real]]: 1589 """Loads a MIPStart (MST) file.""" 1590 f = open(file_name) 1591 result = [] 1592 next(f) 1593 for line in f: 1594 line = line.rstrip().lstrip() 1595 line = " ".join(line.split()) 1596 lc = line.split(" ") 1597 result.append((lc[1], float(lc[2]))) 1598 return result 1599 1600 1601def read_custom_settings(): 1602 global customCbcLib 1603 from pathlib import Path 1604 1605 home = str(Path.home()) 1606 import os 1607 1608 config_path = os.path.join(home, ".config") 1609 if os.path.isdir(config_path): 1610 config_file = os.path.join(config_path, "python-mip") 1611 if os.path.isfile(config_file): 1612 f = open(config_file, "r") 1613 for line in f: 1614 if "=" in line: 1615 cols = line.split("=") 1616 if cols[0].strip().lower() == "cbc-library": 1617 customCbcLib = cols[1].lstrip().rstrip().replace('"', "") 1618 1619 1620logger.info("Using Python-MIP package version {}".format(version)) 1621customCbcLib = "" 1622read_custom_settings() 1623 1624# vim: ts=4 sw=4 et 1625