Exemplo n.º 1
0
class Model:
    """ Mixed Integer Programming Model

    This is the main class, providing methods for building, optimizing,
    querying optimization results and re-optimizing Mixed-Integer Programming
    Models.

    To check how models are created please see the
    :ref:`examples <chapExamples>` included.

    Attributes:
        vars(VarList): list of problem variables (:class:`~mip.model.Var`)
        constrs(ConstrList): list of constraints (:class:`~mip.model.Constr`)

    Examples:
        >>> from mip import Model, MAXIMIZE, CBC, INTEGER, OptimizationStatus
        >>> model = Model(sense=MAXIMIZE, solver_name=CBC)
        >>> x = model.add_var(name='x', var_type=INTEGER, lb=0, ub=10)
        >>> y = model.add_var(name='y', var_type=INTEGER, lb=0, ub=10)
        >>> model += x + y <= 10
        >>> model.objective = x + y
        >>> status = model.optimize(max_seconds=2)
        >>> status == OptimizationStatus.OPTIMAL
        True
    """
    def __init__(
        self: "Model",
        name: str = "",
        sense: str = MINIMIZE,
        solver_name: str = "",
        solver: Optional[Solver] = None,
    ):
        """Model constructor

        Creates a Mixed-Integer Linear Programming Model. The default model
        optimization direction is Minimization. To store and optimize the model
        the MIP package automatically searches and connects in runtime to the
        dynamic library of some MIP solver installed on your computer, nowadays
        gurobi and cbc are supported. This solver is automatically selected,
        but you can force the selection of a specific solver with the parameter
        solver_name.

        Args:
            name (str): model name
            sense (str): MINIMIZATION ("MIN") or MAXIMIZATION ("MAX")
            solver_name(str): gurobi or cbc, searches for which
                solver is available if not informed
            solver(Solver): a (:class:`~mip.solver.Solver`) object; note that
                if this argument is provided, solver_name will be ignored
        """
        self._ownSolver = True
        # initializing variables with default values
        self.solver_name = solver_name
        self.solver = solver  # type: Optional[Solver]

        # reading solver_name from an environment variable (if applicable)
        if not solver:
            if not self.solver_name and "solver_name" in environ:
                self.solver_name = environ["solver_name"]
            if not self.solver_name and "solver_name".upper() in environ:
                self.solver_name = environ["solver_name".upper()]

            # creating a solver instance
            if self.solver_name.upper() in ["GUROBI", "GRB"]:
                from mip.gurobi import SolverGurobi

                self.solver = SolverGurobi(self, name, sense)
            elif self.solver_name.upper() == "CBC":
                from mip.cbc import SolverCbc

                self.solver = SolverCbc(self, name, sense)
            else:
                # checking which solvers are available
                try:
                    from mip.gurobi import SolverGurobi

                    has_gurobi = True
                except ImportError:
                    has_gurobi = False

                if has_gurobi:
                    from mip.gurobi import SolverGurobi

                    self.solver = SolverGurobi(self, name, sense)
                    self.solver_name = GUROBI
                else:
                    from mip.cbc import SolverCbc

                    self.solver = SolverCbc(self, name, sense)
                    self.solver_name = CBC

        # list of constraints and variables
        self.constrs = ConstrList(self)
        self.vars = VarList(self)

        self._status = OptimizationStatus.LOADED

        # initializing additional control variables
        self.__cuts = -1
        self.__cut_passes = -1
        self.__clique = -1
        self.__preprocess = -1
        self.__cuts_generator = None
        self.__lazy_constrs_generator = None
        self.__start = None
        self.__threads = 0
        self.__lp_method = LP_Method.AUTO
        self.__n_cols = 0
        self.__n_rows = 0
        self.__gap = INF
        self.__store_search_progress_log = False
        self.__plog = ProgressLog()
        self.__integer_tol = 1e-6
        self.__infeas_tol = 1e-6
        self.__opt_tol = 1e-6
        self.__max_mip_gap = 1e-4
        self.__max_mip_gap_abs = 1e-10

    def __del__(self: "Model"):
        del self.solver

    def __iadd__(self: "Model", other) -> "Model":
        if isinstance(other, LinExpr):
            if len(other.sense) == 0:
                # adding objective function components
                self.objective = other
            else:
                # adding constraint
                self.add_constr(other)
        elif isinstance(other, tuple):
            if isinstance(other[0], LinExpr) and isinstance(other[1], str):
                if len(other[0].sense) == 0:
                    self.objective = other[0]
                else:
                    self.add_constr(other[0], other[1])

        return self

    def add_var(
        self: "Model",
        name: str = "",
        lb: float = 0.0,
        ub: float = INF,
        obj: float = 0.0,
        var_type: str = CONTINUOUS,
        column: Column = None,
    ) -> Var:
        """ Creates a new variable in the model, returning its reference

        Args:
            name (str): variable name (optional)
            lb (float): variable lower bound, default 0.0
            ub (float): variable upper bound, default infinity
            obj (float): coefficient of this variable in the objective
              function, default 0
            var_type (str): CONTINUOUS ("C"), BINARY ("B") or INTEGER ("I")
            column (Column): constraints where this variable will appear,
                necessary only when constraints are already created in
                the model and a new variable will be created.

        Examples:

            To add a variable :code:`x` which is continuous and greater or
            equal to zero to model :code:`m`::

                x = m.add_var()

            The following code creates a vector of binary variables
            :code:`x[0], ..., x[n-1]` to model :code:`m`::

                x = [m.add_var(var_type=BINARY) for i in range(n)]
        """
        return self.vars.add(name, lb, ub, obj, var_type, column)

    def add_constr(self: "Model", lin_expr: LinExpr, name: str = "") -> Constr:
        """Creates a new constraint (row).

        Adds a new constraint to the model, returning its reference.

        Args:
            lin_expr(LinExpr): linear expression
            name(str): optional constraint name, used when saving model to\
            lp or mps files

        Examples:

        The following code adds the constraint :math:`x_1 + x_2 \leq 1`
        (x1 and x2 should be created first using
        :func:`add_var<mip.model.Model.add_var>`)::

            m += x1 + x2 <= 1

        Which is equivalent to::

            m.add_constr( x1 + x2 <= 1 )

        Summation expressions can be used also, to add the constraint \
        :math:`\displaystyle \sum_{i=0}^{n-1} x_i = y` and name this \
        constraint :code:`cons1`::

            m += xsum(x[i] for i in range(n)) == y, "cons1"

        Which is equivalent to::

            m.add_constr( xsum(x[i] for i in range(n)) == y, "cons1" )
        """

        if isinstance(lin_expr, bool):
            raise InvalidLinExpr("A boolean (true/false) cannot be "
                                 "used as a constraint.")
        return self.constrs.add(lin_expr, name)

    def add_lazy_constr(self: "Model", expr: LinExpr):
        """Adds a lazy constraint

           A lazy constraint is a constraint that is only inserted
           into the model after the first integer solution that violates
           it is found. When lazy constraints are used a restricted
           pre-processing is executed since the complete model is not
           available at the beginning. If the number of lazy constraints
           is too large then they can be added during the search process
           by implementing a
           :class:`~mip.callbacks.ConstrsGenerator` and setting the
           property :attr:`~mip.model.Model.lazy_constrs_generator` of
           :class:`~mip.model.Model`.

           Args:
               expr(LinExpr): the linear constraint
        """
        self.solver.add_lazy_constr(expr)

    def add_sos(self: Solver, sos: List[Tuple[Var, float]], sos_type: int):
        """Adds an Special Ordered Set (SOS) to the model

        In models with binary variables it is often the case that from a list
        of variables only one can receive value 1 in a feasible solution. When
        large constraints of this type exist (packing and partitioning),
        branching in one variable at time usually doesn't work well: while
        fixing one of these variables to one leaves only one possible feasible
        value for the other variables in this set (zero), fixing one variable
        to zero keeps all other variables free. This *unbalanced* branching is
        highly ineffective. A Special ordered set (SOS) is a set
        :math:`\mathcal{S}=\{s_1, s_2, \ldots, s_k\}` with weights
        :math:`[w_1, w_2, \ldots, w_k] \in \mathbb{R}^+`. With this structure
        available branching on a fractional solution :math:`x^*` for these
        variables can be performed computing:


        .. math::

            \min \{ u_{k'} : u_{k'} = | \sum_{j=1\,\ldots \,k'-1}
            w_j \ldotp x^*_j - \sum_{j=k'\,\ldots ,k} w_j \ldotp x^*_j | \}


        Then, branching :math:`\mathcal{S}_1` would be
        :math:`\displaystyle \sum_{j=1, \ldots, k'-1} x_j = 0`
        and
        :math:`\displaystyle \mathcal{S}_2 = \sum_{j=k', \ldots, k} x_j = 0`.

        Args:
            sos(List[Tuple[Var, float]]):
                list including variables (not necessarily binary) and
                respective weights in the model
            sos_type(int):
                1 for Type 1 SOS, where at most one of the binary
                variables can be set to one and 2 for Type 2 SOS, where at
                most two variables from the list may be selected. In type
                2 SOS the two selected variables will be consecutive in
                the list.
        """
        self.solver.add_sos(sos, sos_type)

    def clear(self: Solver):
        """Clears the model

        All variables, constraints and parameters will be reset. In addition,
        a new solver instance will be instantiated to implement the
        formulation.
        """
        # creating a new solver instance
        sense = self.sense

        if self.solver_name.upper() == GUROBI:
            from mip.gurobi import SolverGurobi

            self.solver = SolverGurobi(self, self.name, sense)
        elif self.solver_name.upper() == CBC:
            from mip.cbc import SolverCbc

            self.solver = SolverCbc(self, self.name, sense)
        else:
            # checking which solvers are available
            from mip import gurobi

            if gurobi.found:
                from mip.gurobi import SolverGurobi

                self.solver = SolverGurobi(self, self.name, sense)
                self.solver_name = GUROBI
            else:
                from mip.cbc import SolverCbc

                self.solver = SolverCbc(self, self.name, sense)
                self.solver_name = CBC

        # list of constraints and variables
        self.constrs = ConstrList(self)
        self.vars = VarList(self)

        # initializing additional control variables
        self.__cuts = 1
        self.__cuts_generator = None
        self.__lazy_constrs_generator = None
        self.__start = []
        self._status = OptimizationStatus.LOADED
        self.__threads = 0

    def copy(self: Solver, solver_name: str = "") -> "Model":
        """ Creates a copy of the current model

        Args:
            solver_name(str): solver name (optional)

        Returns:
            clone of current model
        """
        if not solver_name:
            solver_name = self.solver_name
        copy = Model(self.name, self.sense, solver_name)

        # adding variables
        for v in self.vars:
            copy.add_var(name=v.name,
                         lb=v.lb,
                         ub=v.ub,
                         obj=v.obj,
                         var_type=v.var_type)

        # adding constraints
        for c in self.constrs:
            orig_expr = c.expr
            expr = LinExpr(const=orig_expr.const, sense=orig_expr.sense)
            for (var, value) in orig_expr.expr.items():
                expr.add_term(self.vars[var.idx], value)
            copy.add_constr(lin_expr=expr, name=c.name)

        # setting objective function"s constant
        copy.objective_const = self.objective_const

        return copy

    def constr_by_name(self: Solver, name: str) -> Optional[Constr]:
        """ Queries a constraint by its name

        Args:
            name(str): constraint name

        Returns:
            constraint or None if not found
        """
        cidx = self.solver.constr_get_index(name)
        if cidx < 0 or cidx > len(self.constrs):
            return None
        return self.constrs[cidx]

    def var_by_name(self: Solver, name: str) -> Optional[Var]:
        """Searchers a variable by its name

        Returns:
            Variable or None if not found
        """
        v = self.solver.var_get_index(name)
        if v < 0 or v > len(self.vars):
            return None
        return self.vars[v]

    def optimize(
        self: Solver,
        max_seconds: float = INF,
        max_nodes: int = INF,
        max_solutions: int = INF,
    ) -> OptimizationStatus:
        """ Optimizes current model

        Optimizes current model, optionally specifying processing limits.

        To optimize model :code:`m` within a processing time limit of
        300 seconds::

            m.optimize(max_seconds=300)

        Args:
            max_seconds (float): Maximum runtime in seconds (default: inf)
            max_nodes (float): Maximum number of nodes (default: inf)
            max_solutions (float): Maximum number of solutions (default: inf)

        Returns:
            optimization status, which can be OPTIMAL(0), ERROR(-1),
            INFEASIBLE(1), UNBOUNDED(2). When optimizing problems
            with integer variables some additional cases may happen,
            FEASIBLE(3) for the case when a feasible solution was found
            but optimality was not proved, INT_INFEASIBLE(4) for the case
            when the lp relaxation is feasible but no feasible integer
            solution exists and NO_SOLUTION_FOUND(5) for the case when
            an integer solution was not found in the optimization.

        """
        if self.__threads != 0:
            self.solver.set_num_threads(self.__threads)
        # self.solver.set_callbacks(branch_selector,
        # incumbent_updater, lazy_constrs_generator)
        self.solver.set_processing_limits(max_seconds, max_nodes,
                                          max_solutions)

        self._status = self.solver.optimize()
        # has a solution and is a MIP
        if self.num_solutions and self.num_int > 0:
            best = self.objective_value
            lb = self.objective_bound
            if abs(best) <= 1e-10:
                self.__gap = INF
            else:
                self.__gap = abs(best - lb) / abs(best)

        if self.store_search_progress_log:
            self.__plog.log = self.solver.get_log()
            self.__plog.instance = self.name

        return self._status

    def read(self: Solver, path: str):
        """Reads a MIP model or an initial feasible solution.

           One of  the following file name extensions should be used
           to define the contents of what will be loaded:

           :code:`.lp`
             mip model stored in the
             `LP file format <https://www.ibm.com/support/knowledgecenter/SSSA5P_12.9.0/ilog.odms.cplex.help/CPLEX/GettingStarted/topics/tutorials/InteractiveOptimizer/usingLPformat.html>`_

           :code:`.mps`
             mip model stored in the
             `MPS file format <https://en.wikipedia.org/wiki/MPS_(format)>`_

           :code:`.sol`
             initial feasible solution

        Note: if a new problem is readed, all variables, constraints
        and parameters from the current model will be cleared.

        Args:
            path(str): file name
        """
        if not isfile(path):
            raise OSError(2, "File {} does not exists".format(path))

        if path.lower().endswith(".sol") or path.lower().endswith(".mst"):
            mip_start = load_mipstart(path)
            if not mip_start:
                raise Exception("File {} does not contains a valid feasible \
                                 solution.".format(path))
            var_list = []
            for name, value in mip_start:
                var = self.var_by_name(name)
                if var is not None:
                    var_list.append((var, value))
            if not var_list:
                raise Exception("Invalid variable(s) name(s) in \
                                 mipstart file {}".format(path))

            self.start = var_list
            return

        # reading model
        model_ext = [".lp", ".mps"]

        fn_low = path.lower()
        for ext in model_ext:
            if fn_low.endswith(ext):
                self.clear()
                self.solver.read(path)
                self.vars.update_vars(self.solver.num_cols())
                self.constrs.update_constrs(self.solver.num_rows())
                return

        raise Exception("Use .lp, .mps, .sol or .mst as file extension \
                         to indicate the file format.")

    def relax(self: Solver):
        """ Relax integrality constraints of variables

        Changes the type of all integer and binary variables to
        continuous. Bounds are preserved.
        """
        self.solver.relax()

    def write(self: Solver, file_path: str):
        """Saves a MIP model or an initial feasible solution.

           One of  the following file name extensions should be used
           to define the contents of what will be saved:

           :code:`.lp`
             mip model stored in the
             `LP file format <https://www.ibm.com/support/knowledgecenter/SSSA5P_12.9.0/ilog.odms.cplex.help/CPLEX/GettingStarted/topics/tutorials/InteractiveOptimizer/usingLPformat.html>`_

           :code:`.mps`
             mip model stored in the
             `MPS file format <https://en.wikipedia.org/wiki/MPS_(format)>`_

           :code:`.sol`
             initial feasible solution

        Args:
            file_path(str): file name
        """
        if file_path.lower().endswith(".sol") or file_path.lower().endswith(
                ".mst"):
            if self.start:
                save_mipstart(self.start, file_path)
            else:
                mip_start = [(var, var.x) for var in self.vars
                             if abs(var.x) >= 1e-8]
                save_mipstart(mip_start, file_path)
        elif file_path.lower().endswith(".lp") or file_path.lower().endswith(
                ".mps"):
            self.solver.write(file_path)
        else:
            raise Exception("Use .lp, .mps, .sol or .mst as file extension \
                             to indicate the file format.")

    @property
    def objective_bound(self: Solver) -> Optional[float]:
        """
            A valid estimate computed for the optimal solution cost,
            lower bound in the case of minimization, equals to
            :attr:`~mip.model.Model.objective_value` if the
            optimal solution was found.
        """
        if self.status not in [
                OptimizationStatus.OPTIMAL,
                OptimizationStatus.FEASIBLE,
                OptimizationStatus.NO_SOLUTION_FOUND,
        ]:
            return None

        return self.solver.get_objective_bound()

    @property
    def name(self: Solver) -> str:
        """The problem (instance) name

           This name should be used to identify the instance that this model
           refers, e.g.: productionPlanningMay19. This name is stored when
           saving (:meth:`~mip.model.Model.write`) the model in :code:`.LP`
           or :code:`.MPS` file formats.
        """
        return self.solver.get_problem_name()

    @name.setter
    def name(self: Solver, name: str):
        self.solver.set_problem_name(name)

    @property
    def objective(self: Solver) -> LinExpr:
        """The objective function of the problem as a linear expression.

        Examples:

            The following code adds all :code:`x` variables :code:`x[0],
            ..., x[n-1]`, to the objective function of model :code:`m`
            with the same cost :code:`w`::

                m.objective = xsum(w*x[i] for i in range(n))

            A simpler way to define the objective function is the use of the
            model operator += ::

                m += xsum(w*x[i] for i in range(n))

            Note that the only difference of adding a constraint is the lack of
            a sense and a rhs.
        """
        return self.solver.get_objective()

    # TODO how to handle objective += ?
    @objective.setter
    def objective(self: Solver, objective):
        if isinstance(objective, (int, float)):
            self.solver.set_objective(LinExpr([], [], objective))
        elif isinstance(objective, Var):
            self.solver.set_objective(LinExpr([objective], [1]))
        elif isinstance(objective, LinExpr):
            if objective.sense == MAXIMIZE:
                self.solver.set_objective_sense(MAXIMIZE)
            elif objective.sense == MINIMIZE:
                self.solver.set_objective_sense(MINIMIZE)
            self.solver.set_objective(objective)

    @property
    def verbose(self: Solver) -> int:
        """0 to disable solver messages printed on the screen, 1 to enable
        """
        return self.solver.get_verbose()

    @verbose.setter
    def verbose(self: Solver, verbose: int):
        self.solver.set_verbose(verbose)

    @property
    def lp_method(self: Solver) -> LP_Method:
        """Which  method should be used to solve the linear programming
        problem. If the problem has integer variables that this affects only
        the solution of the first linear programming relaxation."""
        return self.__lp_method

    @lp_method.setter
    def lp_method(self: Solver, lpm: LP_Method):
        self.__lp_method = lpm

    @property
    def threads(self: Solver) -> int:
        """number of threads to be used when solving the problem.
        0 uses solver default configuration, -1 uses the number of available
        processing cores and :math:`\geq 1` uses the specified number of
        threads. An increased number of threads may improve the solution
        time but also increases the memory consumption."""
        return self.__threads

    @threads.setter
    def threads(self: Solver, threads: int):
        self.__threads = threads

    @property
    def sense(self: Solver) -> str:
        """ The optimization sense

        Returns:
            the objective function sense, MINIMIZE (default) or (MAXIMIZE)
        """

        return self.solver.get_objective_sense()

    @sense.setter
    def sense(self: Solver, sense: str):
        self.solver.set_objective_sense(sense)

    @property
    def objective_const(self: Solver) -> float:
        """Returns the constant part of the objective function
        """
        return self.solver.get_objective_const()

    @objective_const.setter
    def objective_const(self: Solver, objective_const: float):
        self.solver.set_objective_const(objective_const)

    @property
    def objective_value(self: Solver) -> Optional[float]:
        """Objective function value of the solution found or None
        if model was not optimized
        """
        if self.status not in [
                OptimizationStatus.OPTIMAL,
                OptimizationStatus.FEASIBLE,
        ]:
            return None
        return self.solver.get_objective_value()

    @property
    def gap(self: Solver) -> float:
        """
           The optimality gap considering the cost of the best solution found
           (:attr:`~mip.model.Model.objective_value`)
           :math:`b` and the best objective bound :math:`l`
           (:attr:`~mip.model.Model.objective_bound`) :math:`g` is
           computed as: :math:`g=\\frac{|b-l|}{|b|}`.
           If no solution was found or if :math:`b=0` then :math:`g=\infty`.
           If the optimal solution was found then :math:`g=0`.
        """
        return self.__gap

    @property
    def search_progress_log(self: Solver) -> ProgressLog:
        """
            Log of bound improvements in the search.
            The output of MIP solvers is a sequence of improving
            incumbent solutions (primal bound) and estimates for the optimal
            cost (dual bound). When the costs of these two bounds match the
            search is concluded. In truncated searches, the most common
            situation for hard problems, at the end of the search there is a
            :attr:`~mip.model.Model.gap` between these bounds. This
            property stores the detailed events of improving these
            bounds during the search process. Analyzing the evolution
            of these bounds you can see if you need to improve your
            solver w.r.t. the production of feasible solutions, by including an
            heuristic to produce a better initial feasible solution, for
            example, or improve the formulation with cutting planes, for
            example, to produce better dual bounds. To enable storing the
            :attr:`~mip.model.Model.search_progress_log` set
            :attr:`~mip.model.Model.store_search_progress_log` to True.
        """
        return self.__plog

    @property
    def store_search_progress_log(self: Solver) -> bool:
        """
            Wether :attr:`~mip.model.Model.search_progress_log` will be stored
            or not when optimizing. Default False. Activate it if you want to
            analyze bound improvements over time."""
        return self.__store_search_progress_log

    @store_search_progress_log.setter
    def store_search_progress_log(self: Solver, store: bool):
        self.__store_search_progress_log = store

    # def plot_bounds_evolution(self):
    #    import matplotlib.pyplot as plt
    #    log = self.search_progress_log
    #
    #    # plotting lower bound
    #    x = [a[0] for a in log]
    #    y = [a[1][0] for a in log]
    #    plt.plot(x, y)
    #    # plotting upper bound
    #    x = [a[0] for a in log if a[1][1] < 1e+50]
    #    y = [a[1][1] for a in log if a[1][1] < 1e+50]
    #    plt.plot(x, y)
    #    plt.show()

    @property
    def num_solutions(self: Solver) -> int:
        """Number of solutions found during the MIP search

        Returns:
            number of solutions stored in the solution pool

        """
        return self.solver.get_num_solutions()

    @property
    def objective_values(self: Solver) -> List[float]:
        """List of costs of all solutions in the solution pool

        Returns:
            costs of all solutions stored in the solution pool
            as an array from 0 (the best solution) to
            :attr:`~mip.model.Model.num_solutions`-1.
        """
        return [
            float(self.solver.get_objective_value_i(i))
            for i in range(self.num_solutions)
        ]

    @property
    def cuts_generator(self: Solver) -> "ConstrsGenerator":
        """A cuts generator is an :class:`~mip.callbacks.ConstrsGenerator`
        object that receives a fractional solution and tries to generate one or
        more constraints (cuts) to remove it. The cuts generator is called in
        every node of the branch-and-cut tree where a solution that violates
        the integrality constraint of one or more variables is found.
        """

        return self.__cuts_generator

    @cuts_generator.setter
    def cuts_generator(self: Solver, cuts_generator: ConstrsGenerator):
        self.__cuts_generator = cuts_generator

    @property
    def lazy_constrs_generator(self: Solver) -> "ConstrsGenerator":
        """A lazy constraints generator is an
        :class:`~mip.callbacks.ConstrsGenerator` object that receives
        an integer solution and checks its feasibility. If
        the solution is not feasible then one or more constraints can be
        generated to remove it. When a lazy constraints generator is informed
        it is assumed that the initial formulation is incomplete. Thus, a
        restricted pre-processing routine may be applied. If the initial
        formulation is incomplete, it may be interesting to use the same
        :class:`~mip.callbacks.ConstrsGenerator` to generate cuts *and* lazy
        constraints. The use of *only* lazy constraints may be useful then
        integer solutions rarely violate these constraints.
        """

        return self.__lazy_constrs_generator

    @lazy_constrs_generator.setter
    def lazy_constrs_generator(self: Solver,
                               lazy_constrs_generator: ConstrsGenerator):
        self.__lazy_constrs_generator = lazy_constrs_generator

    @property
    def emphasis(self: Solver) -> SearchEmphasis:
        """defines the main objective of the search, if set to 1 (FEASIBILITY)
        then the search process will focus on try to find quickly feasible
        solutions and improving them; if set to 2 (OPTIMALITY) then the
        search process will try to find a provable optimal solution,
        procedures to further improve the lower bounds will be activated in
        this setting, this may increase the time to produce the first
        feasible solutions but will probably pay off in longer runs;
        the default option if 0, where a balance between optimality and
        feasibility is sought.
        """
        return self.solver.get_emphasis()

    @emphasis.setter
    def emphasis(self: Solver, emphasis: SearchEmphasis):
        self.solver.set_emphasis(emphasis)

    @property
    def preprocess(self: Solver) -> int:
        """Enables/disables pre-processing. Pre-processing tries to improve
        your MIP formulation. -1 means automatic, 0 means off and 1
        means on."""
        return self.__preprocess

    @preprocess.setter
    def preprocess(self: Solver, prep: int):
        self.__preprocess = prep

    @property
    def pump_passes(self: Solver) -> int:
        """Number of passes of the Feasibility Pump :cite:`FGL05` heuristic.
           You may increase this value if you are not getting feasible
           solutions."""
        return self.solver.get_pump_passes()

    @pump_passes.setter
    def pump_passes(self: Solver, passes: int):
        self.solver.set_pump_passes(passes)

    @property
    def cuts(self: Solver) -> int:
        """Controls the generation of cutting planes, -1 means automatic, 0
        disables completely, 1 (default) generates cutting planes in a moderate
        way, 2 generates cutting planes aggressively and 3 generates even more
        cutting planes. Cutting planes usually improve the LP relaxation bound
        but also make the solution time of the LP relaxation larger, so the
        overall effect is hard to predict and experimenting different values
        for this parameter may be beneficial."""

        return self.__cuts

    @cuts.setter
    def cuts(self: Solver, gencuts: int):
        self.__cuts = gencuts

    @property
    def cut_passes(self: Solver) -> int:
        """Maximum number of rounds of cutting planes. You may set this
        parameter to low values if you see that a significant amount of
        time is being spent generating cuts without any improvement in
        the lower bound. -1 means automatic, values greater than zero
        specify the maximum number of rounds."""
        return self.__cut_passes

    @cut_passes.setter
    def cut_passes(self: Solver, cp: int):
        self.__cut_passes = cp

    @property
    def clique(self: Solver) -> int:
        """Controls the generation of clique cuts. -1 means automatic,
        0 disables it, 1 enables it and 2 enables more aggressive clique
        generation."""
        return self.__clique

    @clique.setter
    def clique(self: Solver, clq: int):
        self.__clique = clq

    @property
    def start(self: Solver) -> List[Tuple[Var, float]]:
        """Initial feasible solution

        Enters an initial feasible solution. Only the main binary/integer
        decision variables which appear with non-zero values in the initial
        feasible solution need to be informed. Auxiliary or continuous
        variables are automatically computed.
        """
        return self.__start

    @start.setter
    def start(self: Solver, start: List[Tuple[Var, float]]):
        self.__start = start
        self.solver.set_start(start)

    def validate_mip_start(self: Solver):
        """Validates solution entered in MIPStart

        If the solver engine printed messages indicating that the initial
        feasible solution that you entered in :attr:`~mip.model.start` is not
        valid then you can call this method to help discovering which set of
        variables is causing infeasibility. The current version is quite
        simple: the model is relaxed and one variable entered in mipstart is
        fixed per iteration, indicating if the model still feasible or not.
        """

        out.write("Checking feasibility of MIPStart\n")
        mc = self.copy()
        mc.verbose = 0
        mc.relax()
        mc.optimize()
        if mc.status == OptimizationStatus.INFEASIBLE:
            out.write("Model is infeasible.\n")
            return
        if mc.status == OptimizationStatus.UNBOUNDED:
            out.write("Model is unbounded. You probably need to insert "
                      "additional constraints or bounds in variables.\n")
            return
        if mc.status != OptimizationStatus.OPTIMAL:
            print("Unexpected status while optimizing LP relaxation:"
                  " {}".format(mc.status))

        print("Model LP relaxation bound is {}".format(mc.objective_value))

        for (var, value) in self.start:
            out.write("\tfixing %s to %g ... " % (var.name, value))
            mc += var == value
            mc.optimize()
            if mc.status == OptimizationStatus.OPTIMAL:
                print("ok, obj now: {}".format(mc.objective_value))
            else:
                print("NOT OK, optimization status: {}".format(mc.status))
                return

        print("Linear Programming relaxation of model with fixations from "
              "MIPStart is feasible.")
        print("MIP model may still be infeasible.")

    @property
    def num_cols(self: Solver) -> int:
        """number of columns (variables) in the model"""
        return len(self.vars)

    @property
    def num_int(self: Solver) -> int:
        """number of integer variables in the model"""
        return self.solver.num_int()

    @property
    def num_rows(self: Solver) -> int:
        """number of rows (constraints) in the model"""
        return len(self.constrs)

    @property
    def num_nz(self: Solver) -> int:
        """number of non-zeros in the constraint matrix"""
        return self.solver.num_nz()

    @property
    def cutoff(self: Solver) -> float:
        """upper limit for the solution cost, solutions with cost > cutoff
        will be removed from the search space, a small cutoff value may
        significantly speedup the search, but if cutoff is set to a value too
        low the model will become infeasible"""
        return self.solver.get_cutoff()

    @cutoff.setter
    def cutoff(self: Solver, cutoff: float):
        self.solver.set_cutoff(cutoff)

    @property
    def integer_tol(self: Solver) -> float:
        """Maximum distance to the nearest integer for a variable to be
        considered with an integer value. Default value: 1e-6. Tightening this
        value can increase the numerical precision but also probably increase
        the running time. As floating point computations always involve some
        loss of precision, values too close to zero will likely render some
        models impossible to optimize."""
        return self.__integer_tol

    @integer_tol.setter
    def integer_tol(self: Solver, int_tol: float):
        self.__integer_tol = int_tol

    @property
    def infeas_tol(self: Solver) -> float:
        """Maximum allowed violation for constraints. Default value: 1e-6.
        Tightening this value can increase the numerical precision but also
        probably increase the running time. As floating point computations
        always involve some loss of precision, values too close to zero will
        likely render some models impossible to optimize."""

        return self.__infeas_tol

    @infeas_tol.setter
    def infeas_tol(self: Solver, inf_tol: float):
        self.__infeas_tol = inf_tol

    @property
    def opt_tol(self: Solver) -> float:
        """Maximum reduced cost value for a solution of the LP relaxation to be
        considered optimal. Default value: 1e-6.  Tightening this value can
        increase the numerical precision but also probably increase the running
        time. As floating point computations always involve some loss of
        precision, values too close to zero will likely render some models
        impossible to optimize."""
        return self.__opt_tol

    @opt_tol.setter
    def opt_tol(self: Solver, tol: float):
        self.__opt_tol = tol

    @property
    def max_mip_gap_abs(self: Solver) -> float:
        """Tolerance for the quality of the optimal solution, if a solution
        with cost :math:`c` and a lower bound :math:`l` are available and
        :math:`c-l<` :code:`mip_gap_abs`, the search will be concluded, see
        :attr:`~mip.model.Model.max_mip_gap` to determine a percentage value.
        Default value: 1e-10."""
        return self.__max_mip_gap_abs

    @max_mip_gap_abs.setter
    def max_mip_gap_abs(self: Solver, max_mip_gap_abs: float):
        self.__max_mip_gap_abs = max_mip_gap_abs

    @property
    def max_mip_gap(self: Solver) -> float:
        """value indicating the tolerance for the maximum percentage deviation
        from the optimal solution cost, if a solution with cost :math:`c` and
        a lower bound :math:`l` are available and
        :math:`(c-l)/l <` :code:`max_mip_gap` the search will be concluded.
        Default value: 1e-4."""
        return self.__max_mip_gap

    @max_mip_gap.setter
    def max_mip_gap(self: Solver, max_mip_gap: float):
        self.__max_mip_gap = max_mip_gap

    @property
    def max_seconds(self: Solver) -> float:
        """time limit in seconds for search"""
        return self.solver.get_max_seconds()

    @max_seconds.setter
    def max_seconds(self: Solver, max_seconds: float):
        self.solver.set_max_seconds(max_seconds)

    @property
    def max_nodes(self: Solver) -> int:
        """maximum number of nodes to be explored in the search tree"""
        return self.solver.get_max_nodes()

    @max_nodes.setter
    def max_nodes(self: Solver, max_nodes: int):
        self.solver.set_max_nodes(max_nodes)

    @property
    def max_solutions(self: Solver) -> int:
        """solution limit, search will be stopped when :code:`max_solutions`
        were found"""
        return self.solver.get_max_solutions()

    @max_solutions.setter
    def max_solutions(self: Solver, max_solutions: int):
        self.solver.set_max_solutions(max_solutions)

    @property
    def status(self: Solver) -> OptimizationStatus:
        """ optimization status, which can be OPTIMAL(0), ERROR(-1),
        INFEASIBLE(1), UNBOUNDED(2). When optimizing problems
        with integer variables some additional cases may happen, FEASIBLE(3)
        for the case when a feasible solution was found but optimality was
        not proved, INT_INFEASIBLE(4) for the case when the lp relaxation is
        feasible but no feasible integer solution exists and
        NO_SOLUTION_FOUND(5) for the case when an integer solution was not
        found in the optimization.
        """
        return self._status

    def add_cut(self: Solver, cut: LinExpr):
        """Adds a violated inequality (cutting plane) to the linear programming
        model. If called outside the cut callback performs exactly as
        :meth:`~mip.model.Model.add_constr`. When called inside the cut
        callback the cut is included in the solver's cut pool, which will later
        decide if this cut should be added or not to the model. Repeated cuts,
        or cuts which will probably be less effective, e.g. with a very small
        violation, can be discarded.

        Args:
            cut(LinExpr): violated inequality
        """
        self.solver.add_cut(cut)

    def remove(self: Solver, objects):
        """removes variable(s) and/or constraint(s) from the model

        Args:
            objects: can be a Var, a Constr or a list of these objects
        """
        if isinstance(objects, Var) or isinstance(objects, Constr):
            objects = [objects]

        if isinstance(objects, list):
            vlist = []
            clist = []
            for o in objects:
                if isinstance(o, Var):
                    vlist.append(o)
                elif isinstance(o, Constr):
                    clist.append(o)
                else:
                    raise Exception(
                        "Cannot handle removal of object of type " + type(o) +
                        " from model.")
            if vlist:
                self.vars.remove(vlist)
            if clist:
                self.constrs.remove(clist)
        else:
            raise Exception("Cannot handle removal of object of type " +
                            type(objects) + " from model.")

    def translate(self: Solver, ref) -> Union[List[Any], Dict[Any, Any], Var]:
        """Translates references of variables/containers of variables
        from another model to this model. Can be used to translate
        references of variables in the original model to references
        of variables in the pre-processed model."""

        res = None  # type: Union[List[Any], Dict[Any, Any], Var]

        if isinstance(ref, Var):
            return self.var_by_name(ref.name)
        if isinstance(ref, list):
            res = list()
            for el in ref:
                res.append(self.translate(el))
            return res
        if isinstance(ref, dict):
            res = dict()
            for key, value in ref.items():
                res[key] = self.translate(value)
            return res

        return ref
Exemplo n.º 2
0
class Model:
    """ Mixed Integer Programming Model

    This is the main class, providing methods for building, optimizing,
    querying optimization results and re-optimizing Mixed-Integer Programming
    Models.

    To check how models are created please see the examples included.

    """
    def __init__(self,
                 name: str = "",
                 sense: str = MINIMIZE,
                 solver_name: str = ""):
        """Model constructor

        Creates a Mixed-Integer Linear Programming Model. The default model
        optimization direction is Minimization. To store and optimize the model
        the MIP package automatically searches and connects in runtime to the
        dynamic library of some MIP solver installed on your computer, nowadays
        gurobi and cbc are supported. This solver is automatically selected,
        but you can force the selection of a specific solver with the parameter
        solver_name.

        Args:
            name (str): model name
            sense (str): MINIMIZATION ("MIN") or MAXIMIZATION ("MAX")
            solver_name: gurobi or cbc, searches for which
                solver is available if not informed

        """
        # initializing variables with default values
        self.name = name
        self.solver_name = solver_name
        self.solver = None

        # reading solver_name from an environment variable (if applicable)
        if not self.solver_name and "solver_name" in environ:
            self.solver_name = environ["solver_name"]
        if not self.solver_name and "solver_name".upper() in environ:
            self.solver_name = environ["solver_name".upper()]

        # creating a solver instance
        if self.solver_name.upper() == GUROBI:
            from mip.gurobi import SolverGurobi
            self.solver = SolverGurobi(self, self.name, sense)
        elif self.solver_name.upper() == CBC:
            from mip.cbc import SolverCbc
            self.solver = SolverCbc(self, self.name, sense)
        else:
            # checking which solvers are available
            from mip import gurobi
            if gurobi.has_gurobi:
                from mip.gurobi import SolverGurobi
                self.solver = SolverGurobi(self, self.name, sense)
                self.solver_name = GUROBI
            else:
                from mip.cbc import SolverCbc
                self.solver = SolverCbc(self, self.name, sense)
                self.solver_name = CBC

        # list of constraints and variables
        self.constrs = ConstrList(self)
        self.vars = VarList(self)

        # initializing additional control variables
        self.__cuts = 1
        self.__cuts_generator = None
        self.__lazy_constrs_generator = None
        self.__start = None
        self.__status = OptimizationStatus.LOADED
        self.__threads = 0
        self.__n_cols = 0
        self.__n_rows = 0

    def __del__(self):
        if self.solver:
            del self.solver

    def __iadd__(self, other) -> "Model":
        if isinstance(other, LinExpr):
            if len(other.sense) == 0:
                # adding objective function components
                self.objective = other
            else:
                # adding constraint
                self.add_constr(other)
        elif isinstance(other, tuple):
            if isinstance(other[0], LinExpr) and isinstance(other[1], str):
                if len(other[0].sense) == 0:
                    self.objective = other[0]
                else:
                    self.add_constr(other[0], other[1])

        return self

    def add_var(self,
                name: str = "",
                lb: float = 0.0,
                ub: float = INF,
                obj: float = 0.0,
                var_type: str = CONTINUOUS,
                column: "Column" = None) -> "Var":
        """ Creates a new variable in the model, returning its reference

        Args:
            name (str): variable name (optional)
            lb (float): variable lower bound, default 0.0
            ub (float): variable upper bound, default infinity
            obj (float): coefficient of this variable in the objective
              function, default 0
            var_type (str): CONTINUOUS ("C"), BINARY ("B") or INTEGER ("I")
            column (Column): constraints where this variable will appear,
                necessary only when constraints are already created in
                the model and a new variable will be created.

        Examples:

            To add a variable :code:`x` which is continuous and greater or
            equal to zero to model :code:`m`::

                x = m.add_var()

            The following code creates a vector of binary variables
            :code:`x[0], ..., x[n-1]` to model :code:`m`::

                x = [m.add_var(var_type=BINARY) for i in range(n)]
        """
        if var_type == BINARY:
            lb = 0.0
            ub = 1.0
        if len(name.strip()) == 0:
            nc = self.solver.num_cols()
            name = "C{:011d}".format(nc)
        self.solver.add_var(obj, lb, ub, var_type, column, name)
        self.__n_cols += 1
        return Var(self, self.__n_cols - 1)

    def add_constr(self, lin_expr: "LinExpr", name: str = "") -> "Constr":
        """ Creates a new constraint (row)

        Adds a new constraint to the model, returning its reference

        Args:
            lin_expr (LinExpr): linear expression
            name (str): optional constraint name, used when saving model to\
            lp or mps files

        Examples:

        The following code adds the constraint :math:`x_1 + x_2 \leq 1`
        (x1 and x2 should be created first using
        :func:`add_var<mip.model.Model.add_var>`)::

            m += x1 + x2 <= 1

        Which is equivalent to::

            m.add_constr( x1 + x2 <= 1 )

        Summation expressions can be used also, to add the constraint \
        :math:`\displaystyle \sum_{i=0}^{n-1} x_i = y` and name this \
        constraint :code:`cons1`::

            m += xsum(x[i] for i in range(n)) == y, "cons1"

        Which is equivalent to::

            m.add_constr( xsum(x[i] for i in range(n)) == y, "cons1" )
        """

        if isinstance(lin_expr, bool):
            raise InvalidLinExpr("A boolean (true/false) cannot be \
            used as a constraint.")
        self.__n_rows += 1
        self.solver.add_constr(lin_expr, name)
        return Constr(self, self.__n_rows - 1)

    def clear(self):
        """Clears the model

        All variables, constraints and parameters will be reset. In addition,
        a new solver instance will be instantiated to implement the
        formulation.
        """
        # creating a new solver instance
        sense = self.sense

        self.__n_cols = 0
        self.__n_rows = 0

        if self.solver_name.upper() == GUROBI:
            from mip.gurobi import SolverGurobi
            self.solver = SolverGurobi(self, self.name, sense)
        elif self.solver_name.upper() == CBC:
            from mip.cbc import SolverCbc
            self.solver = SolverCbc(self, self.name, sense)
        else:
            # checking which solvers are available
            from mip import gurobi
            if gurobi.has_gurobi:
                from mip.gurobi import SolverGurobi
                self.solver = SolverGurobi(self, self.name, sense)
                self.solver_name = GUROBI
            else:
                from mip.cbc import SolverCbc
                self.solver = SolverCbc(self, self.name, sense)
                self.solver_name = CBC

        # list of constraints and variables
        self.constrs = ConstrList(self)
        self.vars = VarList(self)

        # initializing additional control variables
        self.__cuts = 1
        self.__cuts_generator = None
        self.__start = []
        self.__status = OptimizationStatus.LOADED
        self.__threads = 0

    def copy(self, solver_name: str = None) -> "Model":
        """ Creates a copy of the current model

        Args:
            solver_name(str): solver name (optional)

        Returns:
            clone of current model
        """
        if not solver_name:
            solver_name = self.solver_name
        copy = Model(self.name, self.sense, solver_name)

        # adding variables
        for v in self.vars:
            copy.add_var(name=v.name,
                         lb=v.lb,
                         ub=v.ub,
                         obj=v.obj,
                         var_type=v.var_type)

        # adding constraints
        for c in self.constrs:
            orig_expr = c.expr
            expr = LinExpr(const=orig_expr.const, sense=orig_expr.sense)
            for (var, value) in orig_expr.expr.items():
                expr.add_term(self.vars[var.idx], value)
            copy.add_constr(lin_expr=expr, name=c.name)

        # setting objective function"s constant
        copy.objective_const = self.objective_const

        return copy

    def constr_by_name(self, name: str) -> "Constr":
        """ Queries a constraint by its name

        Args:
            name(str): constraint name

        Returns:
            constraint or None if not found
        """
        cidx = self.solver.constr_get_index(name)
        if cidx < 0 or cidx > len(self.constrs):
            return None
        return self.constrs[cidx]

    def var_by_name(self, name: str) -> "Var":
        """Searchers a variable by its name

        Returns:
            Variable or None if not found
        """
        v = self.solver.var_get_index(name)
        if v < 0 or v > len(self.vars):
            return None
        return self.vars[v]

    def optimize(self,
                 max_seconds: float = inf,
                 max_nodes: int = inf,
                 max_solutions: int = inf) -> OptimizationStatus:
        """ Optimizes current model

        Optimizes current model, optionally specifying processing limits.

        To optimize model :code:`m` within a processing time limit of
        300 seconds::

            m.optimize(max_seconds=300)

        Args:
            max_seconds (float): Maximum runtime in seconds (default: inf)
            max_nodes (float): Maximum number of nodes (default: inf)
            max_solutions (float): Maximum number of solutions (default: inf)

        Returns:
            optimization status, which can be OPTIMAL(0), ERROR(-1),
            INFEASIBLE(1), UNBOUNDED(2). When optimizing problems
            with integer variables some additional cases may happen,
            FEASIBLE(3) for the case when a feasible solution was found
            but optimality was not proved, INT_INFEASIBLE(4) for the case
            when the lp relaxation is feasible but no feasible integer
            solution exists and NO_SOLUTION_FOUND(5) for the case when
            an integer solution was not found in the optimization.

        """
        if self.__threads != 0:
            self.solver.set_num_threads(self.__threads)
        # self.solver.set_callbacks(branch_selector,
        # incumbent_updater, lazy_constrs_generator)
        self.solver.set_processing_limits(max_seconds, max_nodes,
                                          max_solutions)

        self.__status = self.solver.optimize()

        return self.__status

    def read(self, path: str):
        """Reads a MIP model in :code:`.lp` or :code:`.mps` format.

        Note: all variables, constraints and parameters from the current model
        will be cleared.

        Args:
            path(str): file name
        """
        self.clear()
        self.solver.read(path)
        self.__n_cols = self.solver.num_cols()
        self.__n_rows = self.solver.num_rows()

    def relax(self):
        """ Relax integrality constraints of variables

        Changes the type of all integer and binary variables to
        continuous. Bounds are preserved.
        """
        self.solver.relax()
        for v in self.vars:
            if v.type == BINARY or v.type == INTEGER:
                v.type = CONTINUOUS

    def write(self, path: str):
        """Saves the MIP model, using the extension :code:`.lp` or
        :code:`.mps` to specify the file format.

        Args:
            path(str): file name
        """
        self.solver.write(path)

    @property
    def objective_bound(self) -> float:
        return self.solver.get_objective_bound()

    @property
    def objective(self) -> LinExpr:
        """The objective function of the problem as a linear expression.

        Examples:

            The following code adds all :code:`x` variables :code:`x[0],
            ..., x[n-1]`, to the objective function of model :code:`m`
            with the same cost :code:`w`::

                m.objective = xsum(w*x[i] for i in range(n))

            A simpler way to define the objective function is the use of the
            model operator += ::

                m += xsum(w*x[i] for i in range(n))

            Note that the only difference of adding a constraint is the lack of
            a sense and a rhs.
        """
        return self.solver.get_objective()

    @objective.setter
    def objective(self, objective):
        if isinstance(objective, int) or isinstance(objective, float):
            self.solver.set_objective(LinExpr([], [], objective))
        elif isinstance(objective, Var):
            self.solver.set_objective(LinExpr([objective], [1]))
        elif isinstance(objective, LinExpr):
            self.solver.set_objective(objective)

    @property
    def verbose(self) -> int:
        """0 to disable solver messages printed on the screen, 1 to enable
        """
        return self.solver.get_verbose()

    @verbose.setter
    def verbose(self, verbose: int):
        self.solver.set_verbose(verbose)

    @property
    def threads(self) -> int:
        """number of threads to be used when solving the problem.
        0 uses solver default configuration, -1 uses the number of available
        processing cores and :math:`\geq 1` uses the specified number of
        threads. An increased number of threads may improve the solution
        time but also increases the memory consumption."""
        return self.__threads

    @threads.setter
    def threads(self, threads: int):
        self.__threads = threads

    @property
    def sense(self) -> str:
        """ The optimization sense

        Returns:
            the objective function sense, MINIMIZE (default) or (MAXIMIZE)
        """

        return self.solver.get_objective_sense()

    @sense.setter
    def sense(self, sense: str):
        self.solver.set_objective_sense(sense)

    @property
    def objective_const(self) -> float:
        """Returns the constant part of the objective function
        """
        return self.solver.get_objective_const()

    @objective_const.setter
    def objective_const(self, objective_const: float):
        self.solver.set_objective_const(objective_const)

    @property
    def objective_value(self) -> float:
        """Objective function value of the solution found
        """
        return self.solver.get_objective_value()

    @property
    def num_solutions(self) -> int:
        """Number of solutions found during the MIP search

        Returns:
            number of solutions stored in the solution pool

        """
        return self.solver.get_num_solutions()

    @property
    def objective_values(self) -> List[float]:
        """List of costs of all solutions in the solution pool

        Returns:
            costs of all solutions stored in the solution pool
            as an array from 0 (the best solution) to
            :attr:`~mip.model.model.num_solutions`-1.
        """
        return [
            float(self.solver.get_objective_value_i(i))
            for i in range(self.num_solutions)
        ]

    @property
    def cuts_generator(self) -> "CutsGenerator":
        """Cut generator callback. Cut generators are called whenever a
        solution where one or more integer variables appear with
        continuous values. A cut generator will try to produce
        one or more inequalities to remove this fractional point.
        """
        return self.__cuts_generator

    @cuts_generator.setter
    def cuts_generator(self, cuts_generator: "CutsGenerator"):
        self.__cuts_generator = cuts_generator

    @property
    def lazy_constrs_generator(self) -> "LazyConstrsGenerator":
        return self.__lazy_constrs_generator

    @lazy_constrs_generator.setter
    def lazy_constrs_generator(self,
                               lazy_constrs_generator: "LazyConstrsGenerator"):
        self.__lazy_constrs_generator = lazy_constrs_generator

    @property
    def emphasis(self) -> SearchEmphasis:
        """defines the main objective of the search, if set to 1 (FEASIBILITY)
        then the search process will focus on try to find quickly feasible
        solutions and improving them; if set to 2 (OPTIMALITY) then the
        search process will try to find a provable optimal solution,
        procedures to further improve the lower bounds will be activated in
        this setting, this may increase the time to produce the first
        feasible solutions but will probably pay off in longer runs;
        the default option if 0, where a balance between optimality and
        feasibility is sought.
        """
        return self.solver.get_emphasis()

    @emphasis.setter
    def emphasis(self, emphasis: SearchEmphasis):
        self.solver.set_emphasis(emphasis)

    @property
    def cuts(self) -> int:
        """controls the generation of cutting planes, 0 disables completely,
        1 (default) generates cutting planes in a moderate way,
        2 generates cutting planes aggressively and
        3 generates
        even more cutting planes. Cutting planes usually improve the LP
        relaxation bound but also make the solution time of the LP relaxation
        larger, so the overall effect is hard to predict and experimenting
        different values for this parameter may be beneficial.
        """
        return self.__cuts

    @cuts.setter
    def cuts(self, cuts: int):
        if cuts < 0 or cuts > 3:
            print('Warning: invalid value ({}) for parameter cuts, \
                  keeping old setting.'.format(self.__cuts))
        self.__cuts = cuts

    @property
    def start(self) -> List[Tuple["Var", float]]:
        """Initial feasible solution

        Enters an initial feasible solution. Only the main binary/integer
        decision variables which appear with non-zero values in the initial
        feasible solution need to be informed. Auxiliary or continuous
        variables are automatically computed.
        """
        return self.__start

    @start.setter
    def start(self, start: List[Tuple["Var", float]]):
        self.__start = start
        self.solver.set_start(start)

    @property
    def num_cols(self) -> int:
        """number of columns (variables) in the model"""
        return self.__n_cols

    @property
    def num_int(self) -> int:
        """number of integer variables in the model"""
        return self.solver.num_int()

    @property
    def num_rows(self) -> int:
        """number of rows (constraints) in the model"""
        return self.__n_rows

    @property
    def num_nz(self) -> int:
        """number of non-zeros in the constraint matrix"""
        return self.solver.num_nz()

    @property
    def cutoff(self) -> float:
        """upper limit for the solution cost, solutions with cost > cutoff
        will be removed from the search space, a small cutoff value may
        significantly speedup the search, but if cutoff is set to a value too
        low the model will become infeasible"""
        return self.solver.get_cutoff()

    @cutoff.setter
    def cutoff(self, cutoff: float):
        self.solver.set_cutoff(cutoff)

    @property
    def max_mip_gap_abs(self) -> float:
        """tolerance for the quality of the optimal solution, if a
        solution with cost :math:`c` and a lower bound :math:`l` are available
        and :math:`c-l<` :code:`mip_gap_abs`,
        the search will be concluded, see mip_gap to determine
        a percentage value """
        return self.solver.get_mip_gap_abs()

    @max_mip_gap_abs.setter
    def max_mip_gap_abs(self, max_mip_gap_abs: float):
        self.solver.set_mip_gap(max_mip_gap_abs)

    @property
    def max_mip_gap(self) -> float:
        """value indicating the tolerance for the maximum percentage deviation
        from the optimal solution cost, if a solution with cost :math:`c` and
        a lower bound :math:`l` are available and
        :math:`(c-l)/l <` :code:`max_mip_gap` the search will be concluded."""
        return self.solver.get_mip_gap()

    @max_mip_gap.setter
    def max_mip_gap(self, max_mip_gap: float):
        self.solver.set_mip_gap(max_mip_gap)

    @property
    def max_seconds(self) -> float:
        """time limit in seconds for search"""
        return self.solver.get_max_seconds()

    @max_seconds.setter
    def max_seconds(self, max_seconds: float):
        self.solver.set_max_seconds(max_seconds)

    @property
    def max_nodes(self) -> int:
        """maximum number of nodes to be explored in the search tree"""
        return self.solver.get_max_nodes()

    @max_nodes.setter
    def max_nodes(self, max_nodes: int):
        self.solver.set_max_nodes(max_nodes)

    @property
    def max_solutions(self) -> int:
        """solution limit, search will be stopped when :code:`max_solutions`
        were found"""
        return self.solver.get_max_solutions()

    @max_solutions.setter
    def max_solutions(self, max_solutions: int):
        self.solver.set_max_solutions(max_solutions)

    @property
    def status(self) -> OptimizationStatus:
        """ optimization status, which can be OPTIMAL(0), ERROR(-1),
        INFEASIBLE(1), UNBOUNDED(2). When optimizing problems
        with integer variables some additional cases may happen, FEASIBLE(3)
        for the case when a feasible solution was found but optimality was
        not proved, INT_INFEASIBLE(4) for the case when the lp relaxation is
        feasible but no feasible integer solution exists and
        NO_SOLUTION_FOUND(5) for the case when an integer solution was not
        found in the optimization.
        """
        return self.__status

    def remove(self, objects):
        """removes variable(s) and/or constraint(s) from the model

        Args:
            objects: can be a Var, a Constr or a list of these objects
        """
        if isinstance(objects, Var):
            self.solver.remove_vars([objects.idx])
        elif isinstance(objects, Constr):
            self.solver.remove_constrs([objects.idx])
        elif isinstance(objects, list):
            vlist = []
            clist = []
            for o in objects:
                if isinstance(o, Var):
                    vlist.append(o.idx)
                elif isinstance(o, Constr):
                    clist.append(o.idx)
                else:
                    raise Exception(
                        "Cannot handle removal of object of type " + type(o) +
                        " from model.")
            if vlist:
                vlist.sort()
                self.solver.remove_vars(vlist)
                self.__n_cols -= len(vlist)
            if clist:
                clist.sort()
                self.solver.remove_constrs(clist)
                self.__n_rows -= len(clist)
Exemplo n.º 3
0
class Model:
    """ Mixed Integer Programming Model

    This is the main class, providing methods for building, optimizing,
    querying optimization results and reoptimizing Mixed-Integer Programming
    Models.

    To check how models are created please see the examples included.

    """

    def __init__(self, name: str = "",
                 sense: str = MINIMIZE,
                 solver_name: str = ""):
        """Model constructor

        Creates a Mixed-Integer Linear Programming Model. The default model
        optimization direction is Minimization. To store and optimize the model
        the MIP package automatically searches and connects in runtime to the
        dynamic library of some MIP solver installed on your computer, nowadays
        gurobi and cbc are supported. This solver is automatically selected,
        but you can force the selection of a specific solver with the parameter
        solver_name.

        Args:
            name (str): model name
            sense (str): MINIMIZATION ("MIN") or MAXIMIZATION ("MAX")
            solver_name: gurobi or cbc, searches for which
                solver is available if not informed

        """
        # initializing variables with default values
        self.name = name
        self.solver_name = solver_name
        self.solver = None
        if "solver_name" in environ:
            solver_name = environ["solver_name"]
        if "solver_name".upper() in environ:
            solver_name = environ["solver_name".upper()]

        self.__mipStart = []

        # list of constraints and variables
        self.constrs = []
        self.constrs_by_name = {}
        self.vars = []
        self.vars_by_name = {}
        self.cut_generators = []

        if solver_name.upper() == GUROBI:
            from mip.gurobi import SolverGurobi
            self.solver = SolverGurobi(self, name, sense)
        elif solver_name.upper() == CBC:
            from mip.cbc import SolverCbc
            self.solver = SolverCbc(self, name, sense)
        else:
            # checking which solvers are available
            from mip import gurobi
            if gurobi.has_gurobi:
                from mip.gurobi import SolverGurobi
                self.solver = SolverGurobi(self, name, sense)
                self.solver_name = GUROBI
            else:
                from mip import cbc
                from mip.cbc import SolverCbc
                self.solver = SolverCbc(self, name, sense)
                self.solver_name = CBC

        self.sense = sense

    def __del__(self):
        if self.solver:
            del self.solver

    def __iadd__(self, other) -> "Model":
        if isinstance(other, LinExpr):
            if len(other.sense) == 0:
                # adding objective function components
                self.objective = other
            else:
                # adding constraint
                self.add_constr(other)
        elif isinstance(other, tuple):
            if isinstance(other[0], LinExpr) and isinstance(other[1], str):
                if len(other[0].sense) == 0:
                    self.objective = other[0]
                else:
                    self.add_constr(other[0], other[1])

        return self

    def add_var(self, name: str = "",
                lb: float = 0.0,
                ub: float = INF,
                obj: float = 0.0,
                var_type: str = CONTINUOUS,
                column: "Column" = None) -> "Var":
        """ Creates a new variable

        Adds a new variable to the model.

        Args:
            name (str): variable name (optional)
            lb (float): variable lower bound, default 0.0
            ub (float): variable upper bound, default infinity
            obj (float): coefficient of this variable in the objective function, default 0
            var_type (str): CONTINUOUS ("C"), BINARY ("B") or INTEGER ("I")
            column (Column): constraints where this variable will appear, necessary \
            only when constraints are already created in the model and a new \
            variable will be created.

        Examples:

            To add a variable x which is continuous and greater or equal to zero to model m::

                x = m.add_var()

            The following code creates a vector of binary variables x[0], ..., x[n-1] to model m::

                x = [m.add_var(type=BINARY) for i in range(n)]


        """
        if var_type == BINARY:
            lb = 0.0
            ub = 1.0
        if len(name.strip()) == 0:
            nc = self.solver.num_cols()
            name = "C{:011d}".format(nc)
        idx = self.solver.add_var(obj, lb, ub, var_type, column, name)
        self.vars.append(Var(self, idx, name))
        self.vars_by_name[name] = self.vars[-1]
        return self.vars[-1]

    def add_constr(self, lin_expr: "LinExpr", name: str = "") -> Constr:
        """ Creates a new constraint (row)

        Adds a new constraint to the model

        Args:
            lin_expr (LinExpr): linear expression
            name (str): optional constraint name, used when saving model to\
            lp or mps files

        Examples:

        The following code adds the constraint :math:`x_1 + x_2 \leq 1` 
        (x1 and x2 should be created first using 
        :func:`add_var<mip.model.Model.add_var>`)::

            m += x1 + x2 <= 1

        Which is equivalent to::

            m.add_constr( x1 + x2 <= 1 )

        Summation expressions can be used also, to add the constraint \
        :math:`\displaystyle \sum_{i=0}^{n-1} x_i = y` and name this \
        constraint cons1::

            m += xsum(x[i] for i in range(n)) == y, "cons1"

        """

        if isinstance(lin_expr, bool):
            return None  # empty constraint
        idx = self.solver.add_constr(lin_expr, name)
        self.constrs.append(Constr(self, idx, name))
        self.constrs_by_name[name] = self.constrs[-1]
        return self.constrs[-1]

    def copy(self, solver_name: str = None) -> "Model":
        """ Creates a copy of the current model

        Args:
            solver_name(str): solver name (optional)

        Returns:
            Model: clone of current model

        """
        if not solver_name:
            solver_name = self.solver_name
        copy = Model(self.name, self.sense, solver_name)

        # adding variables
        for v in self.vars:
            copy.add_var(name=v.name, lb=v.lb, ub=v.ub, obj=v.obj, var_type=v.var_type)

        # adding constraints
        for c in self.constrs:
            expr = c.expr  # todo: make copy of constraint"s lin_expr
            copy.add_constr(lin_expr=expr, name=c.name)

        # setting objective function"s constant
        copy.objective_const = self.get_objective_const()

        return copy

    def get_constr_by_name(self, name: str) -> "Constr":
        """ Queries a constraint per name

        Args:
            name(str): constraint name

        Returns:
            Constr: constraint
        """
        return self.constrs_by_name.get(name, None)

    @property
    def objective_bound(self) -> float:
        return self.solver.get_objective_bound()

    @property
    def objective(self) -> LinExpr:
        """LinExpr: Objective function of the problem

        The objective function of the problem as a linear expression.

        Examples:

            The following code adds all x variables x[0], ..., x[n-1], to
            the objective function of model m with weight w::

                m.objective = xsum(w*x[i] for i in range(n))

            A simpler way to define the objective function is the use of the
            model operator += ::

                m += xsum(w*x[i] for i in range(n))

            Note that the only difference of adding a constraint is the lack of
            a sense and a rhs.

        """
        return self.solver.get_objective()

    @objective.setter
    def objective(self, expr):
        if isinstance(expr, int) or isinstance(expr, float):
            self.solver.set_objective(LinExpr([], [], expr))
        elif isinstance(expr, Var):
            self.solver.set_objective(LinExpr([expr], [1]))
        elif isinstance(expr, LinExpr):
            self.solver.set_objective(expr)

    @property
    def sense(self) -> str:
        """ The optimization sense

        Returns:
            str: the objective function sense, MINIMIZE (default) or (MAXIMIZE)
        """

        return self.solver.get_objective_sense()

    @sense.setter
    def sense(self, sense: str):
        self.solver.set_objective_sense(sense)

    @property
    def objective_const(self) -> float:
        """ Returns the current constant part of the objective function

        float: the constant part in the objective function
        """
        return self.solver.get_objective_const()

    @objective_const.setter
    def objective_const(self, const: float) -> None:
        self.solver.set_objective_const(const)

    @property
    def objective_value(self) -> float:
        """ Objective function value

        Returns:
            float: returns the objective function value of the solution found.

        """
        return self.solver.get_objective_value()

    @property
    def num_solutions(self) -> int:
        """ Number of solutions found during the MIP search

        Returns:
            int: number of solutions stored in the solution pool

        """
        return self.solver.get_num_solutions()

    @property
    def objective_values(self) -> List[float]:
        """ List of costs of all solutions in the solution pool

        Returns:
            List[float]: costs of all solutions stored in the solution pool 
            as an array from 0 (the best solution) to num_solutions-1.
        """
        return [float(self.solver.get_objective_value_i(i))\
                 for i in range(self.num_solutions)] 

    def get_var_by_name(self, name) -> "Var":
        """ Searchers a variable by its name

        Returns:
            Var: a reference to a variable
        """
        return self.vars_by_name.get(name, None)

    def relax(self):
        """ Relax integrality constraints of variables

        Changes the type of all integer and binary variables to
        continuous. Bounds are preserved.
        """
        self.solver.relax()
        for v in self.vars:
            if v.var_type == BINARY or v.var_type == INTEGER:
                v.var_type = CONTINUOUS

    def add_cut_generator(self, cuts_generator: "CutsGenerator") -> None:
        """ Adds a cut generator

        Cut generators are called whenever a solution where one or more
        integer variables appear with continuous values. A cut generator will
        try to produce one or more inequalities to remove this fractional point.

        Args:
            cuts_generator : CutsGenerator

        """
        self.cut_generators.append(cuts_generator)

    @property
    def emphasis(self) -> int:
        """int: defines the main objective of the search, if set to 1 (FEASIBILITY) then
        the search process will focus on try to find quickly feasible solutions and
        improving them; if set to 2 (OPTIMALITY) then the search process will try to
        find a provable optimal solution, procedures to further improve the lower bounds will
        be activated in this setting, this may increase the time to produce the first
        feasible solutions but will probably pay off in longer runs; the default option
        if 0, where a balance between optimality and feasibility is sought.
        """
        return self.solver.get_emphasis()

    @emphasis.setter
    def emphasis(self, emph: int):
        self.solver.set_emphasis(emph)

    def optimize(self,
                 branch_selector: "BranchSelector" = None,
                 incumbent_updater: "IncumbentUpdater" = None,
                 lazy_constrs_generator: "LazyConstrsGenerator" = None,
                 max_seconds: float = inf,
                 max_nodes: int = inf,
                 max_solutions: int = inf) -> int:
        """ Optimizes current model

        Optimizes current model, optionally specifying processing limits.

        To optimize model m within a processing time limit of 300 seconds::

            m.optimize(max_seconds=300)

        Args:
            branch_selector (BranchSelector): Callback to select branch (an object of a class inheriting from BranchSelector must be passed)
            cuts_generator (CutsGenerator): Callback to generate cuts (an object of a class inheriting from CutsGenerator must be passed)
            incumbent_updater (IncumbentUpdater): Callback to update incumbent solution (an object of a class inheriting from IncumbentUpdater must be passed)
            lazy_constrs_generator (LazyConstrsGenerator): Callback to include lazy generated constraints (an object of a class inheriting from LazyConstrsGenerator must be passed)
            max_seconds (float): Maximum runtime in seconds (default: inf)
            max_nodes (float): Maximum number of nodes (default: inf)
            max_solutions (float): Maximum number of solutions (default: inf)

        Returns:
            int: optimization status, which can be OPTIMAL(0), ERROR(-1), INFEASIBLE(1), UNBOUNDED(2). When optimizing problems
            with integer variables some additional cases may happen, FEASIBLE(3) for the case when a feasible solution was found
            but optimality was not proved, INT_INFEASIBLE(4) for the case when the lp relaxation is feasible but no feasible integer
            solution exists and NO_SOLUTION_FOUND(5) for the case when an integer solution was not found in the optimization.

        """
        self.solver.set_callbacks(branch_selector, incumbent_updater, lazy_constrs_generator)
        self.solver.set_processing_limits(max_seconds, max_nodes, max_solutions)

        return self.solver.optimize()

    def read(self, path: str) -> None:
        """ Reads a MIP model

        Reads a MIP model in .lp or .mps file format.

        Args:
            path(str): file name

        """
        self.solver.read(path)
        n_cols = self.solver.num_cols()
        n_rows = self.solver.num_rows()
        for i in range(n_cols):
            self.vars.append(Var(self, i, self.solver.var_get_name(i)))
            self.vars_by_name[self.vars[-1].name] = self.vars[-1]
        for i in range(n_rows):
            self.constrs.append(Constr(self, i, self.solver.constr_get_name(i)))
            self.constrs_by_name[self.constrs[-1].name] = self.constrs[-1]
        self.sense = self.solver.get_objective_sense()

    @property
    def start(self) -> List[Tuple["Var", float]]:
        """ Enter an initial feasible solution

        Enters an initial feasible solution. Only the main binary/integer decision variables.
        Auxiliary or continuous variables are automatically computed.

        Args:
            start_sol: list of tuples Var,float indicating non-zero variables
            and their values in the initial feasible solution

        """
        return self.__mipStart

    @start.setter
    def start(self, start_sol: List[Tuple["Var", float]]):
        self.__mipStart = start_sol
        self.solver.set_start(start_sol)

    def write(self, path: str) -> None:
        """ Saves the the MIP model

        Args:
            path(str): file name

        Saves the the MIP model, use the extension ".lp" or ".mps" in the file
        name to specify the file format.

        """
        self.solver.write(path)

    @property
    def num_cols(self) -> int:
        return len(self.vars)

    @property
    def num_rows(self) -> int:
        return len(self.constrs)

    @property
    def cutoff(self) -> float:
        """float: upper limit for the solution cost, solutions with cost > cutoff
        will be removed from the search space, a small cutoff value may significantly
        speedup the search, but if cutoff is set to a value too low
        the model will become infeasible"""
        return self.solver.get_cutoff()

    @cutoff.setter
    def cutoff(self, value: float):
        self.solver.set_cutoff(value)

    @property
    def mip_gap_abs(self) -> float:
        """float: tolerance for the quality of the optimal solution, if a
        solution with cost c and a lower bound l are available and c-l<mip_gap_abs,
        the search will be concluded, see mip_gap to determine
        a percentage value """
        return self.solver.get_mip_gap_abs()

    @mip_gap_abs.setter
    def mip_gap_abs(self, value):
        self.solver.set_mip_gap(value)

    @property
    def mip_gap(self) -> float:
        """float: percentage indicating the tolerance for the maximum percentage deviation
        from the optimal solution cost, if a solution with cost c and a lower bound l
        are available and (c-l)/l < mip_gap the search will be concluded."""
        return self.solver.get_mip_gap()

    @mip_gap.setter
    def mip_gap(self, value):
        self.solver.set_mip_gap(value)

    @property
    def max_seconds(self) -> float:
        """float: time limit in seconds for search"""
        return self.solver.get_max_seconds()

    @max_seconds.setter
    def max_seconds(self, max_seconds: float):
        self.solver.set_max_seconds(max_seconds)

    @property
    def max_nodes(self) -> int:
        """int: maximum number of nodes to be explored in the search tree"""
        return self.solver.get_max_nodes()

    @max_nodes.setter
    def max_nodes(self, max_nodes: int):
        self.solver.set_max_nodes(max_nodes)

    @property
    def max_solutions(self) -> int:
        """int: solution limit, search will be stopped when max_solutions were found"""
        return self.solver.get_max_solutions()

    @max_solutions.setter
    def max_solutions(self, max_solutions: int):
        self.solver.set_max_solutions(max_solutions)