Module miplearn.solvers.internal

Expand source code
#  MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
#  Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
#  Released under the modified BSD license. See COPYING.md for more details.

import logging
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional

from miplearn.instance import Instance
from miplearn.types import (
    LPSolveStats,
    IterationCallback,
    LazyCallback,
    MIPSolveStats,
    VarIndex,
    Solution,
    BranchPriorities,
    Constraint,
)

logger = logging.getLogger(__name__)


class InternalSolver(ABC):
    """
    Abstract class representing the MIP solver used internally by LearningSolver.
    """

    @abstractmethod
    def solve_lp(
        self,
        tee: bool = False,
    ) -> LPSolveStats:
        """
        Solves the LP relaxation of the currently loaded instance. After this
        method finishes, the solution can be retrieved by calling `get_solution`.

        This method should not permanently modify the problem. That is, subsequent
        calls to `solve` should solve the original MIP, not the LP relaxation.

        Parameters
        ----------
        tee
            If true, prints the solver log to the screen.
        """
        pass

    @abstractmethod
    def solve(
        self,
        tee: bool = False,
        iteration_cb: IterationCallback = None,
        lazy_cb: LazyCallback = None,
    ) -> MIPSolveStats:
        """
        Solves the currently loaded instance. After this method finishes,
        the best solution found can be retrieved by calling `get_solution`.

        Parameters
        ----------
        iteration_cb: IterationCallback
            By default, InternalSolver makes a single call to the native `solve`
            method and returns the result. If an iteration callback is provided
            instead, InternalSolver enters a loop, where `solve` and `iteration_cb`
            are called alternatively. To stop the loop, `iteration_cb` should return
            False. Any other result causes the solver to loop again.
        lazy_cb: LazyCallback
            This function is called whenever the solver finds a new candidate
            solution and can be used to add lazy constraints to the model. Only the
            following operations within the callback are allowed:
                - Querying the value of a variable
                - Querying if a constraint is satisfied
                - Adding a new constraint to the problem
            Additional operations may be allowed by specific subclasses.
        tee: bool
            If true, prints the solver log to the screen.
        """
        pass

    @abstractmethod
    def get_solution(self) -> Optional[Solution]:
        """
        Returns current solution found by the solver.

        If called after `solve`, returns the best primal solution found during
        the search. If called after `solve_lp`, returns the optimal solution
        to the LP relaxation. If no primal solution is available, return None.

        The solution is a dictionary `sol`, where the optimal value of `var[idx]`
        is given by `sol[var][idx]`.
        """
        pass

    @abstractmethod
    def set_warm_start(self, solution: Solution) -> None:
        """
        Sets the warm start to be used by the solver.

        The solution should be a dictionary following the same format as the
        one produced by `get_solution`. Only one warm start is supported.
        Calling this function when a warm start already exists will
        remove the previous warm start.
        """
        pass

    @abstractmethod
    def set_instance(
        self,
        instance: Instance,
        model: Any = None,
    ) -> None:
        """
        Loads the given instance into the solver.

        Parameters
        ----------
        instance: Instance
            The instance to be loaded.
        model: Any
            The concrete optimization model corresponding to this instance
            (e.g. JuMP.Model or pyomo.core.ConcreteModel). If not provided,
            it will be generated by calling `instance.to_model()`.
        """
        pass

    @abstractmethod
    def fix(self, solution: Solution) -> None:
        """
        Fixes the values of a subset of decision variables.

        The values should be provided in the dictionary format generated by
        `get_solution`. Missing values in the solution indicate variables
        that should be left free.
        """
        pass

    def set_branching_priorities(self, priorities: BranchPriorities) -> None:
        """
        Sets the branching priorities for the given decision variables.

        When the MIP solver needs to decide on which variable to branch, variables
        with higher priority are picked first, given that they are fractional.
        Ties are solved arbitrarily. By default, all variables have priority zero.

        The priorities should be provided in the dictionary format generated by
        `get_solution`. Missing values indicate variables whose priorities
        should not be modified.
        """
        raise Exception("Not implemented")

    @abstractmethod
    def get_constraint_ids(self) -> List[str]:
        """
        Returns a list of ids which uniquely identify each constraint in the model.
        """
        pass

    @abstractmethod
    def add_constraint(self, cobj: Constraint) -> None:
        """
        Adds a single constraint to the model.
        """
        pass

    @abstractmethod
    def extract_constraint(self, cid: str) -> Constraint:
        """
        Removes a given constraint from the model and returns an object `cobj` which
        can be used to verify if the removed constraint is still satisfied by
        the current solution, using `is_constraint_satisfied(cobj)`, and can potentially
        be re-added to the model using `add_constraint(cobj)`.
        """
        pass

    @abstractmethod
    def is_constraint_satisfied(self, cobj: Constraint) -> bool:
        """
        Returns True if the current solution satisfies the given constraint.
        """
        pass

    @abstractmethod
    def set_constraint_sense(self, cid: str, sense: str) -> None:
        """
        Modifies the sense of a given constraint.

        Parameters
        ----------
        cid: str
            The name of the constraint.
        sense: str
            The new sense (either "<", ">" or "=").
        """
        pass

    @abstractmethod
    def get_constraint_sense(self, cid: str) -> str:
        """
        Returns the sense of a given constraint (either "<", ">" or "=").

        Parameters
        ----------
        cid: str
            The name of the constraint.
        """
        pass

    @abstractmethod
    def get_value(self, var_name: str, index: VarIndex) -> Optional[float]:
        """
        Returns the value of a given variable in the current solution. If no
        solution is available, returns None.
        """
        pass

    @abstractmethod
    def relax(self) -> None:
        """
        Drops all integrality constraints from the model.
        """
        pass

    @abstractmethod
    def get_inequality_slacks(self) -> Dict[str, float]:
        """
        Returns a dictionary mapping constraint name to the constraint slack
        in the current solution.
        """
        pass

    @abstractmethod
    def is_infeasible(self) -> bool:
        """
        Returns True if the model has been proved to be infeasible.
        Must be called after solve.
        """
        pass

    @abstractmethod
    def get_dual(self, cid: str) -> float:
        """
        If the model is feasible and has been solved to optimality, returns the
        optimal value of the dual variable associated with this constraint. If the
        model is infeasible, returns a portion of the infeasibility certificate
        corresponding to the given constraint.

        Only available for relaxed problems. Must be called after solve.
        """
        pass

    @abstractmethod
    def get_sense(self) -> str:
        """
        Returns the sense of the problem (either "min" or "max").
        """
        pass

    @abstractmethod
    def get_empty_solution(self) -> Dict:
        """
        Returns a dictionary with the same shape as the one produced by
        `get_solution`, but with all values set to None. This method is
        used by the ML components to query what variables are there in
        the model before a solution is available.
        """
        pass

Classes

class InternalSolver

Abstract class representing the MIP solver used internally by LearningSolver.

Expand source code
class InternalSolver(ABC):
    """
    Abstract class representing the MIP solver used internally by LearningSolver.
    """

    @abstractmethod
    def solve_lp(
        self,
        tee: bool = False,
    ) -> LPSolveStats:
        """
        Solves the LP relaxation of the currently loaded instance. After this
        method finishes, the solution can be retrieved by calling `get_solution`.

        This method should not permanently modify the problem. That is, subsequent
        calls to `solve` should solve the original MIP, not the LP relaxation.

        Parameters
        ----------
        tee
            If true, prints the solver log to the screen.
        """
        pass

    @abstractmethod
    def solve(
        self,
        tee: bool = False,
        iteration_cb: IterationCallback = None,
        lazy_cb: LazyCallback = None,
    ) -> MIPSolveStats:
        """
        Solves the currently loaded instance. After this method finishes,
        the best solution found can be retrieved by calling `get_solution`.

        Parameters
        ----------
        iteration_cb: IterationCallback
            By default, InternalSolver makes a single call to the native `solve`
            method and returns the result. If an iteration callback is provided
            instead, InternalSolver enters a loop, where `solve` and `iteration_cb`
            are called alternatively. To stop the loop, `iteration_cb` should return
            False. Any other result causes the solver to loop again.
        lazy_cb: LazyCallback
            This function is called whenever the solver finds a new candidate
            solution and can be used to add lazy constraints to the model. Only the
            following operations within the callback are allowed:
                - Querying the value of a variable
                - Querying if a constraint is satisfied
                - Adding a new constraint to the problem
            Additional operations may be allowed by specific subclasses.
        tee: bool
            If true, prints the solver log to the screen.
        """
        pass

    @abstractmethod
    def get_solution(self) -> Optional[Solution]:
        """
        Returns current solution found by the solver.

        If called after `solve`, returns the best primal solution found during
        the search. If called after `solve_lp`, returns the optimal solution
        to the LP relaxation. If no primal solution is available, return None.

        The solution is a dictionary `sol`, where the optimal value of `var[idx]`
        is given by `sol[var][idx]`.
        """
        pass

    @abstractmethod
    def set_warm_start(self, solution: Solution) -> None:
        """
        Sets the warm start to be used by the solver.

        The solution should be a dictionary following the same format as the
        one produced by `get_solution`. Only one warm start is supported.
        Calling this function when a warm start already exists will
        remove the previous warm start.
        """
        pass

    @abstractmethod
    def set_instance(
        self,
        instance: Instance,
        model: Any = None,
    ) -> None:
        """
        Loads the given instance into the solver.

        Parameters
        ----------
        instance: Instance
            The instance to be loaded.
        model: Any
            The concrete optimization model corresponding to this instance
            (e.g. JuMP.Model or pyomo.core.ConcreteModel). If not provided,
            it will be generated by calling `instance.to_model()`.
        """
        pass

    @abstractmethod
    def fix(self, solution: Solution) -> None:
        """
        Fixes the values of a subset of decision variables.

        The values should be provided in the dictionary format generated by
        `get_solution`. Missing values in the solution indicate variables
        that should be left free.
        """
        pass

    def set_branching_priorities(self, priorities: BranchPriorities) -> None:
        """
        Sets the branching priorities for the given decision variables.

        When the MIP solver needs to decide on which variable to branch, variables
        with higher priority are picked first, given that they are fractional.
        Ties are solved arbitrarily. By default, all variables have priority zero.

        The priorities should be provided in the dictionary format generated by
        `get_solution`. Missing values indicate variables whose priorities
        should not be modified.
        """
        raise Exception("Not implemented")

    @abstractmethod
    def get_constraint_ids(self) -> List[str]:
        """
        Returns a list of ids which uniquely identify each constraint in the model.
        """
        pass

    @abstractmethod
    def add_constraint(self, cobj: Constraint) -> None:
        """
        Adds a single constraint to the model.
        """
        pass

    @abstractmethod
    def extract_constraint(self, cid: str) -> Constraint:
        """
        Removes a given constraint from the model and returns an object `cobj` which
        can be used to verify if the removed constraint is still satisfied by
        the current solution, using `is_constraint_satisfied(cobj)`, and can potentially
        be re-added to the model using `add_constraint(cobj)`.
        """
        pass

    @abstractmethod
    def is_constraint_satisfied(self, cobj: Constraint) -> bool:
        """
        Returns True if the current solution satisfies the given constraint.
        """
        pass

    @abstractmethod
    def set_constraint_sense(self, cid: str, sense: str) -> None:
        """
        Modifies the sense of a given constraint.

        Parameters
        ----------
        cid: str
            The name of the constraint.
        sense: str
            The new sense (either "<", ">" or "=").
        """
        pass

    @abstractmethod
    def get_constraint_sense(self, cid: str) -> str:
        """
        Returns the sense of a given constraint (either "<", ">" or "=").

        Parameters
        ----------
        cid: str
            The name of the constraint.
        """
        pass

    @abstractmethod
    def get_value(self, var_name: str, index: VarIndex) -> Optional[float]:
        """
        Returns the value of a given variable in the current solution. If no
        solution is available, returns None.
        """
        pass

    @abstractmethod
    def relax(self) -> None:
        """
        Drops all integrality constraints from the model.
        """
        pass

    @abstractmethod
    def get_inequality_slacks(self) -> Dict[str, float]:
        """
        Returns a dictionary mapping constraint name to the constraint slack
        in the current solution.
        """
        pass

    @abstractmethod
    def is_infeasible(self) -> bool:
        """
        Returns True if the model has been proved to be infeasible.
        Must be called after solve.
        """
        pass

    @abstractmethod
    def get_dual(self, cid: str) -> float:
        """
        If the model is feasible and has been solved to optimality, returns the
        optimal value of the dual variable associated with this constraint. If the
        model is infeasible, returns a portion of the infeasibility certificate
        corresponding to the given constraint.

        Only available for relaxed problems. Must be called after solve.
        """
        pass

    @abstractmethod
    def get_sense(self) -> str:
        """
        Returns the sense of the problem (either "min" or "max").
        """
        pass

    @abstractmethod
    def get_empty_solution(self) -> Dict:
        """
        Returns a dictionary with the same shape as the one produced by
        `get_solution`, but with all values set to None. This method is
        used by the ML components to query what variables are there in
        the model before a solution is available.
        """
        pass

Ancestors

  • abc.ABC

Subclasses

Methods

def add_constraint(self, cobj)

Adds a single constraint to the model.

Expand source code
@abstractmethod
def add_constraint(self, cobj: Constraint) -> None:
    """
    Adds a single constraint to the model.
    """
    pass
def extract_constraint(self, cid)

Removes a given constraint from the model and returns an object cobj which can be used to verify if the removed constraint is still satisfied by the current solution, using is_constraint_satisfied(cobj), and can potentially be re-added to the model using add_constraint(cobj).

Expand source code
@abstractmethod
def extract_constraint(self, cid: str) -> Constraint:
    """
    Removes a given constraint from the model and returns an object `cobj` which
    can be used to verify if the removed constraint is still satisfied by
    the current solution, using `is_constraint_satisfied(cobj)`, and can potentially
    be re-added to the model using `add_constraint(cobj)`.
    """
    pass
def fix(self, solution)

Fixes the values of a subset of decision variables.

The values should be provided in the dictionary format generated by get_solution. Missing values in the solution indicate variables that should be left free.

Expand source code
@abstractmethod
def fix(self, solution: Solution) -> None:
    """
    Fixes the values of a subset of decision variables.

    The values should be provided in the dictionary format generated by
    `get_solution`. Missing values in the solution indicate variables
    that should be left free.
    """
    pass
def get_constraint_ids(self)

Returns a list of ids which uniquely identify each constraint in the model.

Expand source code
@abstractmethod
def get_constraint_ids(self) -> List[str]:
    """
    Returns a list of ids which uniquely identify each constraint in the model.
    """
    pass
def get_constraint_sense(self, cid)

Returns the sense of a given constraint (either "<", ">" or "=").

Parameters

cid : str
The name of the constraint.
Expand source code
@abstractmethod
def get_constraint_sense(self, cid: str) -> str:
    """
    Returns the sense of a given constraint (either "<", ">" or "=").

    Parameters
    ----------
    cid: str
        The name of the constraint.
    """
    pass
def get_dual(self, cid)

If the model is feasible and has been solved to optimality, returns the optimal value of the dual variable associated with this constraint. If the model is infeasible, returns a portion of the infeasibility certificate corresponding to the given constraint.

Only available for relaxed problems. Must be called after solve.

Expand source code
@abstractmethod
def get_dual(self, cid: str) -> float:
    """
    If the model is feasible and has been solved to optimality, returns the
    optimal value of the dual variable associated with this constraint. If the
    model is infeasible, returns a portion of the infeasibility certificate
    corresponding to the given constraint.

    Only available for relaxed problems. Must be called after solve.
    """
    pass
def get_empty_solution(self)

Returns a dictionary with the same shape as the one produced by get_solution, but with all values set to None. This method is used by the ML components to query what variables are there in the model before a solution is available.

Expand source code
@abstractmethod
def get_empty_solution(self) -> Dict:
    """
    Returns a dictionary with the same shape as the one produced by
    `get_solution`, but with all values set to None. This method is
    used by the ML components to query what variables are there in
    the model before a solution is available.
    """
    pass
def get_inequality_slacks(self)

Returns a dictionary mapping constraint name to the constraint slack in the current solution.

Expand source code
@abstractmethod
def get_inequality_slacks(self) -> Dict[str, float]:
    """
    Returns a dictionary mapping constraint name to the constraint slack
    in the current solution.
    """
    pass
def get_sense(self)

Returns the sense of the problem (either "min" or "max").

Expand source code
@abstractmethod
def get_sense(self) -> str:
    """
    Returns the sense of the problem (either "min" or "max").
    """
    pass
def get_solution(self)

Returns current solution found by the solver.

If called after solve, returns the best primal solution found during the search. If called after solve_lp, returns the optimal solution to the LP relaxation. If no primal solution is available, return None.

The solution is a dictionary sol, where the optimal value of var[idx] is given by sol[var][idx].

Expand source code
@abstractmethod
def get_solution(self) -> Optional[Solution]:
    """
    Returns current solution found by the solver.

    If called after `solve`, returns the best primal solution found during
    the search. If called after `solve_lp`, returns the optimal solution
    to the LP relaxation. If no primal solution is available, return None.

    The solution is a dictionary `sol`, where the optimal value of `var[idx]`
    is given by `sol[var][idx]`.
    """
    pass
def get_value(self, var_name, index)

Returns the value of a given variable in the current solution. If no solution is available, returns None.

Expand source code
@abstractmethod
def get_value(self, var_name: str, index: VarIndex) -> Optional[float]:
    """
    Returns the value of a given variable in the current solution. If no
    solution is available, returns None.
    """
    pass
def is_constraint_satisfied(self, cobj)

Returns True if the current solution satisfies the given constraint.

Expand source code
@abstractmethod
def is_constraint_satisfied(self, cobj: Constraint) -> bool:
    """
    Returns True if the current solution satisfies the given constraint.
    """
    pass
def is_infeasible(self)

Returns True if the model has been proved to be infeasible. Must be called after solve.

Expand source code
@abstractmethod
def is_infeasible(self) -> bool:
    """
    Returns True if the model has been proved to be infeasible.
    Must be called after solve.
    """
    pass
def relax(self)

Drops all integrality constraints from the model.

Expand source code
@abstractmethod
def relax(self) -> None:
    """
    Drops all integrality constraints from the model.
    """
    pass
def set_branching_priorities(self, priorities)

Sets the branching priorities for the given decision variables.

When the MIP solver needs to decide on which variable to branch, variables with higher priority are picked first, given that they are fractional. Ties are solved arbitrarily. By default, all variables have priority zero.

The priorities should be provided in the dictionary format generated by get_solution. Missing values indicate variables whose priorities should not be modified.

Expand source code
def set_branching_priorities(self, priorities: BranchPriorities) -> None:
    """
    Sets the branching priorities for the given decision variables.

    When the MIP solver needs to decide on which variable to branch, variables
    with higher priority are picked first, given that they are fractional.
    Ties are solved arbitrarily. By default, all variables have priority zero.

    The priorities should be provided in the dictionary format generated by
    `get_solution`. Missing values indicate variables whose priorities
    should not be modified.
    """
    raise Exception("Not implemented")
def set_constraint_sense(self, cid, sense)

Modifies the sense of a given constraint.

Parameters

cid : str
The name of the constraint.
sense : str
The new sense (either "<", ">" or "=").
Expand source code
@abstractmethod
def set_constraint_sense(self, cid: str, sense: str) -> None:
    """
    Modifies the sense of a given constraint.

    Parameters
    ----------
    cid: str
        The name of the constraint.
    sense: str
        The new sense (either "<", ">" or "=").
    """
    pass
def set_instance(self, instance, model=None)

Loads the given instance into the solver.

Parameters

instance : Instance
The instance to be loaded.
model : Any
The concrete optimization model corresponding to this instance (e.g. JuMP.Model or pyomo.core.ConcreteModel). If not provided, it will be generated by calling instance.to_model().
Expand source code
@abstractmethod
def set_instance(
    self,
    instance: Instance,
    model: Any = None,
) -> None:
    """
    Loads the given instance into the solver.

    Parameters
    ----------
    instance: Instance
        The instance to be loaded.
    model: Any
        The concrete optimization model corresponding to this instance
        (e.g. JuMP.Model or pyomo.core.ConcreteModel). If not provided,
        it will be generated by calling `instance.to_model()`.
    """
    pass
def set_warm_start(self, solution)

Sets the warm start to be used by the solver.

The solution should be a dictionary following the same format as the one produced by get_solution. Only one warm start is supported. Calling this function when a warm start already exists will remove the previous warm start.

Expand source code
@abstractmethod
def set_warm_start(self, solution: Solution) -> None:
    """
    Sets the warm start to be used by the solver.

    The solution should be a dictionary following the same format as the
    one produced by `get_solution`. Only one warm start is supported.
    Calling this function when a warm start already exists will
    remove the previous warm start.
    """
    pass
def solve(self, tee=False, iteration_cb=None, lazy_cb=None)

Solves the currently loaded instance. After this method finishes, the best solution found can be retrieved by calling get_solution.

Parameters

iteration_cb : IterationCallback
By default, InternalSolver makes a single call to the native solve method and returns the result. If an iteration callback is provided instead, InternalSolver enters a loop, where solve and iteration_cb are called alternatively. To stop the loop, iteration_cb should return False. Any other result causes the solver to loop again.
lazy_cb : LazyCallback
This function is called whenever the solver finds a new candidate solution and can be used to add lazy constraints to the model. Only the following operations within the callback are allowed: - Querying the value of a variable - Querying if a constraint is satisfied - Adding a new constraint to the problem Additional operations may be allowed by specific subclasses.
tee : bool
If true, prints the solver log to the screen.
Expand source code
@abstractmethod
def solve(
    self,
    tee: bool = False,
    iteration_cb: IterationCallback = None,
    lazy_cb: LazyCallback = None,
) -> MIPSolveStats:
    """
    Solves the currently loaded instance. After this method finishes,
    the best solution found can be retrieved by calling `get_solution`.

    Parameters
    ----------
    iteration_cb: IterationCallback
        By default, InternalSolver makes a single call to the native `solve`
        method and returns the result. If an iteration callback is provided
        instead, InternalSolver enters a loop, where `solve` and `iteration_cb`
        are called alternatively. To stop the loop, `iteration_cb` should return
        False. Any other result causes the solver to loop again.
    lazy_cb: LazyCallback
        This function is called whenever the solver finds a new candidate
        solution and can be used to add lazy constraints to the model. Only the
        following operations within the callback are allowed:
            - Querying the value of a variable
            - Querying if a constraint is satisfied
            - Adding a new constraint to the problem
        Additional operations may be allowed by specific subclasses.
    tee: bool
        If true, prints the solver log to the screen.
    """
    pass
def solve_lp(self, tee=False)

Solves the LP relaxation of the currently loaded instance. After this method finishes, the solution can be retrieved by calling get_solution.

This method should not permanently modify the problem. That is, subsequent calls to solve should solve the original MIP, not the LP relaxation.

Parameters

tee
If true, prints the solver log to the screen.
Expand source code
@abstractmethod
def solve_lp(
    self,
    tee: bool = False,
) -> LPSolveStats:
    """
    Solves the LP relaxation of the currently loaded instance. After this
    method finishes, the solution can be retrieved by calling `get_solution`.

    This method should not permanently modify the problem. That is, subsequent
    calls to `solve` should solve the original MIP, not the LP relaxation.

    Parameters
    ----------
    tee
        If true, prints the solver log to the screen.
    """
    pass