Ejemplo 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 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)
Ejemplo n.º 2
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)
Ejemplo 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)