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