Beispiel #1
0
class Model:

    def __init__(self, name: str = "",
                 sense: str = MINIMIZE,
                 solver_name: str = ''):
        # initializing variables with default values
        self.name: str = name
        self.sense: str = sense
        self.solver_name: str = solver_name
        self.solver: Solver = None

        # list of constraints and variables
        self.constrs: List[Constr] = []
        self.vars: List[Var] = []

        if solver_name.upper() == GUROBI:
            from mip.gurobi import SolverGurobi
            self.solver = SolverGurobi(self, name, sense)
        elif solver_name.upper() == CBC:
            from mip.cbc import SolverCbc
            self.solver = SolverCbc(self, name, sense)
        else:
            # search for the best solver available
            if gurobi.has_gurobi:
                from mip.gurobi import SolverGurobi
                self.solver = SolverGurobi(self, name, sense)
            elif cbc.has_cbc:
                from mip.cbc import SolverCbc
                self.solver = SolverCbc(self, name, sense)

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

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

        return self

    def add_var(self, name: str = "",
                lb: float = 0.0,
                ub: float = INF,
                obj: float = 0.0,
                type: str = CONTINUOUS,
                column: "Column" = None) -> "Var":
        if len(name.strip()) == 0:
            nc = self.solver.num_cols()
            name = 'C{:011d}'.format(nc)
        idx = self.solver.add_var(obj, lb, ub, type, column, name)
        self.vars.append(Var(self, idx, name))
        return self.vars[-1]

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

    def copy(self, solver_name: str = None) -> "Model":
        if not solver_name:
            solver_name = self.solver_name
        copy: Model = Model(self.name, self.sense, solver_name)

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

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

        # setting objective function's constant
        copy.set_objective_const(self.get_objective_const())

        return copy

    def get_objective(self) -> LinExpr:
        return self.solver.get_objective()

    def get_objective_const(self) -> float:
        return self.solver.get_objective_const()

    def optimize(self,
                 maxSeconds=inf,
                 maxNodes=inf,
                 maxSolutions=inf
                 ) -> int:
        if maxSeconds != inf or maxNodes != inf or maxSolutions != inf:
            self.solver.set_processing_limits(maxSeconds, maxNodes, maxSolutions)
        self.solver.optimize()

    def get_objective_value(self) -> float:
        return self.solver.get_objective_value()

    def set_start(self, variables: List["Var"], values: List[float]):
        self.solver.set_start(variables, values)

    def set_objective(self, expr, sense: str = "") -> None:
        if isinstance(expr, int) or isinstance(expr, float):
            self.solver.set_objective(LinExpr([], [], expr))
        elif isinstance(expr, Var):
            self.solver.set_objective(LinExpr([expr], [1]))
        elif isinstance(expr, LinExpr):
            self.solver.set_objective(expr, sense)

    def set_objective_const(self, const: float) -> None:
        return self.solver.set_objective_const(const)

    def write(self, path: str) -> None:
        self.solver.write(path)

    def read(self, path: str) -> None:
        self.solver.read(path)
        nCols = self.solver.num_cols()
        nRows = self.solver.num_rows()
        for i in range(nCols):
            self.vars.append(Var(self, i, self.solver.var_get_name(i)))
        for i in range(nRows):
            self.constrs.append(Constr(self, i, self.solver.constr_get_name(i)))

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

    @property
    def num_rows(self) -> int:
        return len(self.constrs)
Beispiel #2
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)